Authentication and token verification weakness (XL-013)
Updated 2026-05-15What this is
A signed token has two parts that matter: the claims (who the bearer says they are) and the signature (proof the claims were issued by your server and not edited). Verification is the step that checks the signature against your key. Decoding is not verification. A token whose claims you can read is not a token whose signature you have checked.
Two patterns skip the check:
algorithm: 'none'(quoted or unquoted). The "none" algorithm is a real JWT algorithm that means "unsigned." A token signed withnonehas no signature to forge. An attacker writes{ "sub": "admin" }, sets the algorithm tonone, and your server accepts it.jwt.verify(token)with no key argument. Depending on the library and version, a verify call with no key may decode without checking the signature at all, which is the same outcome asnone.
The library surface differs by language, the bug does not:
- Node:
jsonwebtokenjwt.verify(token)with no secret, or{ algorithm: 'none' }in sign options. - Python:
jwt.decode(token, options={"verify_signature": False}), PyJWTalgorithms=["none"]. - Java: a
JwtParserbuilt withoutsetSigningKey. - Ruby:
JWT.decode(token, nil, false).
Same family (XL-013), one concept: the signature was never checked.
Why AI emits it
Asked to "make auth work," a model reaches for the shortest code that returns a usable token and a populated user object. Both of these patterns do exactly that. The demo logs in, the user object is correct, nothing throws. The failure mode only appears when someone deliberately forges a token, and there is no test for that in a prototype, so the hole ships invisibly.
The mental model that produces the bug
"The token decodes and the user comes back, so auth works." There is no mental model separating decode from verify. The token round-trips in local testing because local testing never sends a forged one.
What the fix looks like
Verify with a real algorithm and a real key, every time.
- Node:
jwt.verify(token, process.env.JWT_SECRET)for HS256, or the public key for RS256. Never{ algorithm: 'none' }. - Pin the accepted algorithms explicitly: pass
{ algorithms: ['HS256'] }so a token claimingnoneor a swapped algorithm is rejected before the signature step. - Python:
jwt.decode(token, key, algorithms=["HS256"]), neververify_signature: Falseoutside a test.
The signature is checked against your key on every request. A token your server did not sign does not verify, regardless of what its claims say.
Related
- Hardcoded secrets and policy text is the matching failure on the key side: a strong verify step does not help if the signing secret is committed to the repo.
RELATED PROBES
- · JS Auth Token Verification (XL-013)