The basic JWT Tampering Lab showed the alg:none attack and trivial tampering. This bonus level is harder, more interesting, and still found in real-world bug bounties — the RS256-to-HS256 key-confusion attack.
The setup: the server uses RS256 (RSA signatures) and publishes its public key. The verification library checks the alg field in the token header and dispatches to either an RSA verify or an HMAC verify. If the attacker changes alg to HS256 and signs the token using the RSA public key as the HMAC secret, the verify path uses that same public key as the HMAC key — and the signature matches.
// Heliotrope SSO · token verification const jwt = require("jsonwebtoken"); const publicKey = fs.readFileSync("./public.pem"); // shipped with the app function verifyToken(token) { // BUG: no explicit algorithm list. The library reads alg from the token header. return jwt.verify(token, publicKey); }
The server issued this RS256-signed token after a normal login as alice. The signature is valid; the role is employee.
{
"alg": "RS256",
"typ": "JWT"
}{
"sub": "alice@heliotrope.com",
"user_id": 1001,
"role": "employee",
"iat": 1717592000,
"exp": 1717595600
}RSA-SHA256 over header + "." + payload, signed with private key. (opaque to us — we don't have the private key)
Goal: elevate role to admin and have the server accept it.
The public key is published — that's what public means. Common publishing locations: a .well-known/jwks.json endpoint, an OAuth discovery document, the app's GitHub repo, the docs site. Often you can also extract it from any valid token if the API responds with a JWK in the header.
Anyone can have this. It is published for a reason. The server expects this key will be used only to verify RSA signatures — not to sign anything.
Modify the payload to set role: "admin". Change the header's alg from RS256 to HS256. Compute the HMAC-SHA256 signature using the public key bytes as the HMAC secret.
The server reads alg: HS256 from the header and dispatches to the HMAC verify path. It uses publicKey (the variable) as the key — which is now being interpreted as an HMAC secret. Your HMAC signature matches.
Why this works
JWT libraries take the algorithm from the token header by default and dispatch to the corresponding verify routine. The routine is told which key to use — but the routine doesn't check that the key matches the algorithm. For RSA, the key is expected to be an RSA public key. For HMAC, the key is expected to be a shared secret — just bytes.
An RSA public key is also just bytes. When the HMAC verify routine receives publicKey (the variable that holds the PEM-encoded RSA public key), it treats those bytes as an HMAC secret. The attacker signed their forged token with the same bytes, so the HMAC check passes.
The root cause is the library's "smart" algorithm dispatch — specifically, that the application code does not specify which algorithms it expects. The server intended to use RSA; the library obediently switched to HMAC the moment the token's header asked it to.
The fix
Two fixes, applied together:
- Pin the algorithm in the verify call. Every modern JWT library accepts an explicit algorithm list:
jwt.verify(token, publicKey, { algorithms: ['RS256'] });With this, the library rejects any token whosealgis not in the list — including HS256. - Use different key material for different algorithms. The RSA key for RS256; a separate, never-published, high-entropy secret for HS256 if you use it elsewhere. Don't share keys across primitives.
Most JWT libraries written in the last few years default to algorithm-pinning. The legacy default of "read alg from header" is responsible for years of CVEs across virtually every language ecosystem.
The key confusion attack is one of the most elegant bugs in identity systems: the cryptographic primitive itself accepts the attacker's substitution because the verifier doesn't check that the key matches the algorithm. The fix is a single keyword argument. The mistake costs entire applications.
The general lesson generalizes beyond JWT: when a protocol negotiates an algorithm, the verifying party must pin which algorithms are acceptable. Otherwise the attacker picks the algorithm that they have the key for, and the verifier obediently follows.
References
Formatted in APA 7. Alphabetized by first author's last name.
- Auth0. (2015). Critical vulnerabilities in JSON Web Token libraries. https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
- Jones, M., Bradley, J., & Sakimura, N. (2015). JSON Web Token (JWT) (Request for Comments No. 7519). Internet Engineering Task Force. https://datatracker.ietf.org/doc/html/rfc7519
- OWASP Foundation. (n.d.). JSON Web Token for Java cheat sheet. https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html