A JSON Web Token carries claims that a server trusts, and a signature that is supposed to prove those claims were not edited. A JWT algorithm confusion attack abuses the one field that decides how that signature gets checked. When a server reads the algorithm name out of the token and obeys it, an attacker can pick an algorithm the server never intended, and forge a token the server accepts as genuine.
The three parts of a JWT
A signed JWT is three base64url segments joined by dots: header.payload.signature. The header and payload are JSON. The signature is computed over the first two parts. RFC 7519 defines the token shape, and RFC 7515 (JSON Web Signature) defines how the signature is produced and verified.
# A typical token for the app acme.example (decoded view, not a real secret)
header = {"alg": "RS256", "typ": "JWT"}
payload = {"sub": "1042", "role": "user", "iss": "acme.example", "exp": 1750800000}
signature = RSASSA-PKCS1-v1_5( base64url(header) + "." + base64url(payload), private_key )
# On the wire it looks like this (truncated, illustrative only)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDQyIiwicm9sZSI6InVzZXIifQ.SflKx...
The role claim here is what an attacker wants to change from user to admin. The signature is the only thing stopping them. So the whole question becomes: how does the server check that signature, and can the attacker influence the answer.
Why trusting the header alg is the root flaw
The alg field in the header tells the verifier which algorithm to use. RS256 means an RSA signature, verified with a public key. HS256 means an HMAC, verified with a shared secret. A careless library reads alg from the token and runs whatever it finds. That hands the attacker control of the verification path. CWE-347, improper verification of a cryptographic signature, is the formal name for the resulting bug class.
The token is asking the server a question, which key should I be checked with, and the server should never let the token answer it.
The alg:none variant
RFC 7518 defines a value of none for the algorithm, meaning the token is unsecured and carries no signature at all. It exists for narrow cases where another layer already provides integrity. The problem is a server that still accepts it on a normal authenticated route.
An attacker sets the header to {"alg":"none"}, edits the payload to grant themselves an admin role, and sends the token with an empty signature segment:
# alg:none token (header and payload only, third segment is empty)
{"alg":"none","typ":"JWT"} . {"sub":"1042","role":"admin"} .
If the server skips signature checking because the algorithm says there is nothing to check, the forged claims sail through. The fix is plain: reject none on any route that requires a signed token.
The RS256 to HS256 key confusion variant
This is the sharper version of a JWT algorithm confusion attack, and it works even when the server uses real signatures. Picture acme.example issuing RS256 tokens. The server holds an RSA private key for signing and an RSA public key for verifying. The public key is not a secret. It might sit in a JWKS endpoint, in documentation, or in a mobile app bundle.
Now the attacker forges a token with the header set to {"alg":"HS256"}. HS256 is symmetric: the same key both signs and verifies. The attacker computes an HMAC over their edited header and payload, using the RSA public key string as the HMAC secret. Then they send it.
If the server reads alg from the token and switches to HS256, it goes looking for the HMAC secret. In a vulnerable setup it reaches for the only key it has on hand, the RSA public key, and uses that exact string as the secret. It recomputes the HMAC over the same bytes, gets the same value the attacker computed, and the signature matches. The token is accepted.
The trick rests on one fact. The attacker forged a valid signature using only public information, because in this confused path the verification secret is the public key, and the public key is known to everyone. Auth0 and PortSwigger both documented this pattern, and it remains a common finding in token handling code.
A short example of the shape
# What the attacker controls: the header and payload
header = {"alg":"HS256","typ":"JWT"}
payload = {"sub":"1042","role":"admin","iss":"acme.example"}
# The forged signature is an HMAC keyed by the RSA PUBLIC key text
signature = HMAC_SHA256( signing_input, rsa_public_key_pem )
# Vulnerable server: reads alg=HS256 from the token, verifies HMAC
# using the same rsa_public_key_pem it normally uses for RSA verify.
# The two HMAC values match, so the forged token is trusted.
No private key was ever needed. The attacker at evil.example only needed the public key text that acme.example was already giving out.
How to spot it
- Read the header. Decode a real token and look at
alg. If the issuer uses RS256 but the server also accepts HS256 ornoneon the same route, that mismatch is the warning sign. You can inspect a token’s algorithm and claims with our free JWT security inspector, an in browser tool where nothing you paste leaves the page. - Try the swaps in a test environment. Against an app you own, change
algtononewith an empty signature, and separately try an HS256 token signed with the published public key. If either is accepted, the server is trusting the header. - Audit the verify call. Search the code for the verification function. If it derives the algorithm from the token instead of pinning an expected list, that is the bug in source form.
How to prevent a JWT algorithm confusion attack
- Pin the expected algorithm on the server. Pass an explicit allowlist such as
["RS256"]to the verify call. Never derive the algorithm from the incoming token. - Reject alg:none. Treat
noneas invalid on every authenticated route. Do not rely on a library default. - Use separate keys per algorithm. A key meant for RSA verification should never be reachable as an HMAC secret. Keep symmetric and asymmetric material in different stores so a confused code path cannot grab the wrong one.
- Verify before reading claims. Check the signature first and only then read
role,sub, or anything else. A token that fails verification should be discarded before any claim is trusted. - Keep libraries current. Many JWT libraries hardened these defaults years ago. Older versions still ship the foot guns.
This bug lives next to broader authorization mistakes, so it is worth reading our access control category for the wider pattern. It also pairs well with understanding authentication vs authorization, since a forged token attacks both at once.
Why this rewards understanding the app
You do not find a JWT algorithm confusion attack by replaying a fixed payload. You find it by understanding which algorithm the issuer uses, where the public key is exposed, and whether the verify call pins what it expects. The bug is an assumption, that the token would never lie about how to check itself, and the way to find it is to test that assumption directly.
That is the kind of bug an autonomous researcher that tests an app’s assumptions is built to surface. You can read more about that approach on our about page.
Frequently asked questions
What is a JWT algorithm confusion attack?
It is an attack where a server reads the alg field out of a JSON Web Token and obeys it, letting the attacker pick how the signature is verified. By choosing an algorithm the server never intended, such as none or HS256 instead of RS256, the attacker forges a token the server accepts. The formal bug class is improper verification of a cryptographic signature, described in MITRE CWE 347.
How does the RS256 to HS256 key confusion attack work?
A server that should verify RS256 tokens uses an RSA public key, which is not secret. The attacker forges a token with the header set to {"alg":"HS256"} and computes an HMAC over it using that public key text as the secret. If the server reads alg from the token and switches to HS256, it verifies the HMAC with the same public key, the values match, and the forged token is trusted. No private key is ever needed.
What is the alg:none bug in JWTs?
RFC 7518 defines an algorithm value of none for unsecured tokens that carry no signature. The bug is a server that still accepts none on a route requiring a signed token. An attacker sets the header to {"alg":"none"}, edits the payload to grant an admin role, and sends an empty signature segment. A server that skips checking because the algorithm says there is nothing to check will trust the forged claims.
How do you prevent a JWT algorithm confusion attack?
Pin an explicit algorithm allowlist such as ["RS256"] on the verify call and never derive the algorithm from the incoming token. Reject none on every authenticated route, keep symmetric and asymmetric keys in separate stores so a confused path cannot use a public key as an HMAC secret, and verify the signature before reading any claim. Keeping JWT libraries current also helps, since many hardened these defaults years ago.
