JWT Algorithm Confusion Attack: RS256 to HS256 Explained
Algorithm confusion is simple on paper: the server expects an asymmetric JWT (RS256) but a library path will happily treat the PEM public key as an HMAC secret if you flip alg to HS256. You sign with the public key. Verification “succeeds.” You are whoever you claim in the payload.
Stop there if the service pins alg server-side or uses a modern library that forbids this mix. Many older stacks still do not.
Why the confusion exists
RS256 = sign with private RSA key, verify with public key. HS256 = same secret for sign and verify. If verification code does:
- Read
algfrom the attacker-controlled header - Load “the key” (meant to be the RSA public key)
- Call HMAC-SHA256 verify with that key bytes
…then possession of the public key is enough to forge tokens. Public keys are often in JWKS URLs, mobile apps, or well-known endpoints. That is not a separate “key leak” finding; it is expected material used the wrong way.
Practical forge steps
Decode a legitimate access token. Note kid, claims, and that alg is RS256. Fetch the JWKS or embedded PEM. Build a new header with "alg":"HS256" (sometimes keep the same kid, sometimes strip it — try both). Set sub / user_id / role to the victim or to admin, matching what the API trusts. Sign with HMAC-SHA256 using the public key bytes exactly as the library would load them (PEM string vs raw DER — mismatches fail closed and waste hours).
Send the forged token on an authenticated API call. Prefer an endpoint that echoes identity (/me, /whoami) before touching destructive actions.
Library and config smells
- Explicit allow of multiple algs including HS* and RS*
- Custom verify wrappers that pass
keywithout algorithm allowlists - “Flexible JWT” middleware written years ago and never revisited
- Microservices that verify with a shared JWKS helper copied from a blog post
PortSwigger’s JWT algorithm confusion labs mirror this pattern; work them before spraying production.
False starts I keep seeing
Forging with the wrong PEM encoding. Changing only alg without resigning. Assuming none and confusion are the same bug (they are neighbors, not twins). Reporting “JWT accepts HS256” without a forged token that changes authorization — that is a weak or incomplete report.
Impact language
Lead with account impersonation or privilege escalation, not cryptography trivia. Show victim user id in a response body. If refresh tokens or service tokens share the same verifier, say so. Map to broken authentication / CWE-347 (improper verification of cryptographic signature) when the writeup needs a CWE.
What defenders should ship
Ignore alg from the token; decide the algorithm from config. Separate key objects for HMAC vs asymmetric verify. Prefer libraries that reject algorithm confusion by default. Rotate and monitor for tokens that suddenly arrive as HS* on an RS*-only service.
If you can download the public key and log in as another user, you are done proving the class. Everything else is severity dressing.