I recently audited a heavily funded FinTech startup's shiny new API. They had done everything "right" on paper: zero-trust architecture, mutual TLS between microservices, and strict OAuth 2.0 implementations. But when I looked at their stateless authentication mechanism, I found a vulnerability that was first documented in 2015.
By simply changing the alg header in their JSON Web Token (JWT) and signing it with their own public key, I bypassed their entire authentication gateway.
We like to think that modern API hacking requires complex chains of business logic flaws. But the reality is that fundamental cryptographic implementation errors, like JWT Algorithm Confusion, are still rampant in 2026. Here's why this keeps happening and how you can exploit—and fix—it.
The Root Cause: Trusting the Header
The JSON Web Token standard (RFC 7519) is incredibly flexible, perhaps to a fault. The first part of a JWT is the Header, which contains the alg (algorithm) parameter telling the server how the token's signature was generated.
// A standard ES256 Header { "alg": "ES256", "typ": "JWT" }
When an application uses asymmetric encryption (like ES256 or RS256), the server uses a Private Key to sign the token and a Public Key to verify it. Anyone can have the public key, but only the server has the private key.
However, when an application uses symmetric encryption (like HS256), the server uses a single Secret Key to both sign and verify the token.
Algorithm confusion occurs when a backend library allows the attacker to dictate the algorithm via the token's header, rather than enforcing a specific algorithm based on the endpoint's configuration.
How the attacker uses the server's public key as an HMAC secret to forge a valid signature.
The Attack Chain
If you find an application using asymmetric keys (RS256 or ES256), the attack goes like this:
- Obtain the Public Key: Since it's a public key, the application often exposes it via an endpoint like
/.well-known/jwks.json. - Modify the Token: Decode the JWT header and change
"alg": "ES256"to"alg": "HS256". - Elevate Privileges: Modify the payload to make yourself an admin (e.g.,
"role": "admin"). - Sign the Token: Here is the magic. You sign the modified token using the
HS256(symmetric) algorithm, but you use the Public Key as your secret string!
When the vulnerable server receives the token, it reads the header: "alg": "HS256". It then passes the token and its configured key (the Public Key) to its verification function. Because the function is now expecting a symmetric HMAC signature, it uses the Public Key as the secret string. Since you also used the Public Key as the secret string to sign it, the signature matches perfectly.
Exploitation Example
Here is a Python script that automates this forgery for an authorized testing scenario.
import jwt # PyJWT library import requests # 1. Fetch the target's public key jwks_response = requests.get("https://api.target.com/.well-known/jwks.json") public_key_pem = extract_pem_from_jwks(jwks_response.json()) # 2. Define the forged payload payload = { "user_id"
This simple script completely dismantled the authentication layer of a modern application. It's a stark reminder that when you're hunting business logic flaws, you should never overlook the cryptographic foundations.
Why Is This Still Happening in 2026?
You'd think modern JWT libraries would have patched this. And they have! If you use the latest versions of jsonwebtoken in Node or PyJWT in Python, you are generally safe.
The problem is the explosion of microservices and custom internal gateways. Engineering teams often write lightweight, custom JWT verification middleware to save milliseconds of latency on edge functions or WebAssembly (Wasm) proxies. In doing so, they reimplement the exact same vulnerability from scratch. They parse the unverified header to decide which Verify() path to take, blinding themselves to the downgrade.
How to Defend Against It
The fix is incredibly simple: Never trust the alg header.
The algorithm should be strictly hardcoded in your verification function. Your code should dictate the algorithm, not the user-supplied token.
// VULNERABLE PATTERN: Letting the library trust the token's header const decoded = jwt.verify(token, publicKey); // SECURE PATTERN: Explicitly forcing the expected algorithm const decoded = jwt.verify(token, publicKey, { algorithms: [
When building OAuth and OIDC architectures, ensure your identity provider and your resource servers explicitly enforce asymmetric algorithms.
JWTs are powerful, but they are foot-guns. Always enforce the algorithm, rotate your keys, and for the love of God, don't write your own cryptography parsing logic in an edge worker.
References / Further reading
- IETF RFC 7519: JSON Web Token (JWT) - https://datatracker.ietf.org/doc/html/rfc7519
- PortSwigger: JWT vulnerabilities and algorithm confusion - https://portswigger.net/web-security/jwt
- Auth0: Critical Vulnerabilities in JSON Web Token Libraries (2015) - https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
- PyJWT Documentation: Enforcing Algorithms - https://pyjwt.readthedocs.io/en/stable/usage.html



