Bots & traffic
8 min readUpdatedMathematical CAPTCHA: how it works, how to build one and where it fails
Quick answer
A mathematical CAPTCHA asks the visitor to solve a small sum, such as 4 plus 7, before a form is accepted. It stops only the crudest form bots: any script that can read the question can calculate the answer, and arithmetic is a barrier for people with dyscalculia. Built well, the answer stays on the server, each question works once, and it sits behind lighter checks such as honeypot fields and rate limits.
01What is a mathematical CAPTCHA?
A mathematical CAPTCHA, also called a math or arithmetic CAPTCHA, is a CAPTCHA whose challenge is a small calculation, such as 'What is 4 plus 7?', that the visitor must answer before a form is accepted. Variants write the numbers as words, render the sum as an image, or swap arithmetic for a simple logic question.
Its appeal is practical. It needs no third-party script, sets no tracking cookies, works without JavaScript and takes a few lines of server code, which is why it is common on the contact, comment and newsletter forms of small sites. What it buys in security is far less than it appears.
02Why are math CAPTCHAs easy for bots?
Because a question a person can read is a question a program can read. A sum written as text is trivial for a script to parse and calculate; rendering it as an image only adds a character recognition step, and published attacks on distorted text reach under a second per image at up to 99.8% accuracy. Worded questions such as 'seven plus two' are no harder for software that reads text.
The research record is old and consistent. In 2010 Hernandez-Castro and Ribagorda showed in Computers & Security that a deployed math CAPTCHA could be completely solved by a low-cost attack on its design and implementation flaws, with no character recognition or artificial intelligence involved. The W3C's note on the inaccessibility of CAPTCHA says logic problems can be largely cracked by simple as well as complex algorithms, and that puzzles at best raise the bar enough that bots move elsewhere.
A small answer space adds a second weakness. With two numbers from 1 to 9 there are only 17 possible sums, and the most likely one, 10, comes up in 9 of the 81 combinations. Even without reading the question, guessing the most common sum succeeds 11.1% of the time, so a design that accepts several guesses per question is weaker still.
| Implementation mistake | What it allows | Fix |
|---|---|---|
| The answer, or a hash of it, is in the page, or the check runs only in JavaScript | Any client can read the answer or skip the check | Keep the answer on the server and verify every submission there |
| A solved question can be submitted again | One solve is replayed for thousands of posts | Make each question single-use |
| Several answers are accepted per question | Guessing through the small answer space | Burn the question on the first attempt, right or wrong |
| Questions never expire | Solved questions are stockpiled for later | Expire questions after a few minutes |
| A question works on any form | An easy form supplies passes for a valuable one | Bind each question to one form or session |
03Are math CAPTCHAs accessible?
Less than they look. WCAG 2.2 defines a cognitive function test as a task to remember, manipulate or transcribe information and names the performance of calculations as an example. Under success criterion 3.3.8 at level AA, such a test may not be required in any step of a login unless another method or a helping mechanism is available, and the W3C's Understanding document uses a math CAPTCHA as an example of a test that is not excepted.
On a contact or comment form, which is not authentication, 3.3.8 does not apply, but the barrier remains. The W3C note on CAPTCHA points out that someone living with dyscalculia will find even simple arithmetic challenging, and recommends offering a different kind of puzzle on request. A sum rendered only as an image also shuts out screen-reader users unless it has a text alternative, and a text alternative that states the sum removes the point of the image.
- Never put a math CAPTCHA in front of a login without another way to sign in
- Write the question as real text with a proper label, not as an image
- Use a numeric keyboard on phones, and accept the answer with surrounding spaces
- Offer a different kind of question, or a contact route, for anyone who cannot answer
- Explain a wrong answer clearly and keep the rest of the form filled in
04How to build a math CAPTCHA on the server
Two designs work. The simplest stores the answer in the visitor's server-side session and deletes it on the first attempt. The stateless design below suits pages served from a cache or from several servers: the question travels with a signed token, so no session is needed, and a small shared record of used tokens provides the single-use guarantee.
The token carries an expiry, a random nonce and two HMAC-SHA256 signatures. The first proves the server issued the token for this form; the second binds the correct answer without revealing it, because without the secret key nobody can compute the signature for a guess. Verification checks the first signature, rejects expired or already used tokens, marks the nonce as used before comparing, and compares with hmac.compare_digest, which Python documents as designed to prevent timing analysis. The numbers come from the secrets module, which Python recommends over random for anything security-related.
import hashlib, hmac, secrets, time
KEY = secrets.token_bytes(32) # in production: a fixed secret loaded from config
TTL = 600 # seconds a question stays valid
used = {} # nonce -> expiry; in production: a shared store with expiry
def _mac(*parts):
msg = "|".join(map(str, parts)).encode()
return hmac.new(KEY, msg, hashlib.sha256).hexdigest()
def new_challenge(form):
a, b = secrets.randbelow(9) + 1, secrets.randbelow(9) + 1
exp, nonce = int(time.time()) + TTL, secrets.token_hex(16)
tag = _mac("issued", exp, nonce, form) # proves this server issued the token
sig = _mac("answer", a + b, exp, nonce, form) # binds the answer without revealing it
return f"What is {a} plus {b}?", f"{exp}.{nonce}.{tag}.{sig}"
def verify(token, answer, form):
if not token.isascii():
return False # compare_digest raises on non-ASCII text
try:
exp_s, nonce, tag, sig = token.split(".")
exp, guess = int(exp_s), int(answer.strip())
except ValueError:
return False
now = time.time()
if not hmac.compare_digest(tag, _mac("issued", exp, nonce, form)):
return False # forged, altered or other form
if exp < now or nonce in used:
return False # expired or already tried
used[nonce] = exp # burn first: one attempt per question
for n in [n for n, e in used.items() if e < now]:
del used[n]
return hmac.compare_digest(sig, _mac("answer", guess, exp, nonce, form))05What to use instead of, or before, a math CAPTCHA
Order the checks by what they cost real visitors. The cheapest stop most unsophisticated form bots on their own, and each later step should only see what the earlier ones let through. OWASP's bot management cheat sheet describes the hidden honeypot field in detail and treats visible CAPTCHAs as a last resort.
| Check | Cost to real visitors | What it stops | Weakness |
|---|---|---|---|
| Honeypot field | None: the field is hidden from people and from assistive technology | Bots that fill in every field | Bots that skip hidden fields; browser autofill can occasionally fill it |
| Time trap | None at normal typing speed | Instant scripted submissions | Scripts that simply wait |
| Rate limits per address and account | None in normal use | Bulk submissions from a few sources | Traffic spread over many addresses |
| Server-side content checks | Occasional misfires on messages with links | Link spam and known spam text | Needs tuning and review |
| Proof-of-work challenge | A short pause, nothing to solve | Makes cheap high-volume automation costlier | Does not prove a human; real browsers pass |
| Math CAPTCHA | A small task, and a barrier for some disabilities | Naive form bots | Any client that reads the question |
| Interactive third-party CAPTCHA | A task, plus a third-party script | More capable bots | Solving services, plus privacy and accessibility costs |
06When is a math CAPTCHA the right choice?
Rarely as the only control, sometimes as a fallback. It suits low-value forms on small sites that want no third-party code, ideally shown only after a honeypot or rate limit has flagged a submission. It does not suit anything an attacker can profit from.
| Situation | Math CAPTCHA? | Better option |
|---|---|---|
| Contact or comment form on a small site | Acceptable as a fallback | Honeypot and time trap first; the math question only for flagged posts |
| Login form | No | Rate limits, MFA and breached-password checks, since WCAG 3.3.8 rules out puzzles without an alternative; see credential stuffing |
| Sign-up form hit by bulk registrations | No | Email confirmation, rate limits and a background challenge |
| Checkout or payment form | No | The payment provider's fraud checks and per-card limits |
| A site flooded with requests | No | Filtering in front of the origin; see HTTP floods |
07What a WAF or reverse proxy can add
A web application firewall or reverse proxy in front of the site can take load and abuse off the form before the application sees it, but it cannot judge whether a message is spam.
- Can: rate-limit submissions to the form's URL per client and refuse request methods the form does not use
- Can: challenge clients that look automated before they reach the form, so the math question is rarely needed
- Can: refuse sources already seen abusing other sites behind the same service
- Cannot: read the meaning of a message, or tell an angry customer from a spammer
- Cannot: repair a CAPTCHA whose answer leaks in the page or whose tokens can be replayed
Questions
Frequently asked questions
Sources
Sources and verification
The Python example was run with Python 3.11.2 on Debian 12 in October 2026: a correct answer passed once, while replays, wrong answers, retries after a wrong answer, expired or altered tokens, tokens containing non-ASCII characters and tokens used on another form were rejected. The guessing figure was computed by enumerating all 81 pairs.
- 01Web Content Accessibility Guidelines (WCAG) 2.2: cognitive function test · W3C
- 02Understanding Success Criterion 3.3.8: Accessible Authentication (Minimum) · W3C WAI
- 03Inaccessibility of CAPTCHA: Alternatives to Visual Turing Tests on the Web (section 2.1.3, Logic Puzzles) · W3C
- 04Pitfalls in CAPTCHA Design and Implementation: The Math CAPTCHA, a Case Study (abstract) · Universidad Carlos III de Madrid
- 05Hernandez-Castro and Ribagorda, Computers & Security 29 (2010) 141-157 · Elsevier
- 06An Empirical Study & Evaluation of Modern CAPTCHAs (Searles et al., USENIX Security 2023) · USENIX
- 07Bot Management and Anti-Automation Cheat Sheet · OWASP Cheat Sheet Series
- 08hmac: Keyed-Hashing for Message Authentication · Python documentation
- 09secrets: Generate secure random numbers for managing secrets · Python documentation
Last updated