JWT Attack Techniques: alg:none, Key Confusion, and Weak Secrets
A technical guide to JWT attack techniques including algorithm confusion, the alg:none bypass, brute-forcing weak secrets, and claim injection with defenses.
JWT Attack Techniques: alg:none, Key Confusion, and Weak Secrets
JSON Web Tokens (JWTs) are used extensively for authentication and authorization in web applications. Their portability and self-contained nature make them attractive, but these same properties make them a high-value attack target. A valid JWT is often the difference between unauthenticated access and full account or admin access. This guide covers the most impactful JWT attack techniques and the defenses that stop them.
JWT Structure Recap
A JWT consists of three base64url-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJ1c2VyIn0.signature
[header] .[payload] .[signature]
The header specifies the algorithm. The payload contains claims. The signature is computed by the issuer and verified by the recipient. Every attack described here exploits some failure in signature verification.
The alg:none Attack
The alg:none algorithm is defined in the JWT specification as a way to issue unsigned tokens. When a server accepts alg:none, an attacker can forge arbitrary tokens by stripping the signature entirely.
Attack steps:
- Decode the header and payload of a legitimate token.
- Modify the header to set
"alg": "none". - Modify the payload (e.g., change
"role": "user"to"role": "admin"). - Re-encode both parts and append an empty signature (just a trailing dot).
# Original
eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoidXNlciJ9.signature
# Forged
eyJhbGciOiJub25lIn0.eyJyb2xlIjoiYWRtaW4ifQ.
Many early JWT libraries accepted this because they followed the specification literally. Modern libraries disable none by default, but the vulnerability persists in:
- Libraries configured with
algorithms: ['HS256', 'none'] - Homegrown JWT verification code
- Libraries where
nonemust be explicitly disabled
Fix: Explicitly specify the expected algorithm(s) during verification and reject everything else, including none:
import jwt from 'jsonwebtoken';
// Vulnerable — accepts algorithm from token header
const decoded = jwt.verify(token, secret);
// Safe — enforces specific algorithm
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
# python-jose / PyJWT
import jwt
# Vulnerable
decoded = jwt.decode(token, secret)
# Safe
decoded = jwt.decode(token, secret, algorithms=['HS256'])
Algorithm Confusion (RS256 → HS256)
Algorithm confusion attacks exploit servers that support both asymmetric (RS256/ES256) and symmetric (HS256) algorithms. The attack works like this:
- The server issues RS256 tokens, signed with a private key. The corresponding public key is published (e.g., at a JWKS endpoint).
- A vulnerable server accepts HS256 tokens and verifies them using the configured key.
- If the verification code uses the same key for both algorithms, an attacker can fetch the public key and use it as the HS256 secret to forge tokens.
- The server receives an HS256 token, treats the public key as the HMAC secret, and successfully verifies the forged signature.
This attack works because jwt.verify(token, key) is ambiguous — the behavior depends on the algorithm in the token header. If the server does not enforce a specific algorithm, the attacker controls which algorithm is used for verification.
Demonstration (attack scenario):
import jwt
import requests
# 1. Fetch the server's public key from JWKS endpoint
jwks = requests.get('https://api.example.com/.well-known/jwks.json').json()
public_key = extract_pem_from_jwks(jwks)
# 2. Sign a forged token using the public key as an HMAC secret
forged_payload = {'sub': 'user_123', 'role': 'admin'}
forged_token = jwt.encode(forged_payload, public_key, algorithm='HS256')
# 3. Send to server — if it uses the same key for both algorithms, verification passes
Fix: Enforce the algorithm used during verification. Never derive the algorithm from the token header:
import { jwtVerify, importSPKI } from 'jose';
// For RS256: always pass the public key AND specify RS256
async function verifyToken(token: string): Promise<JWTPayload> {
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY, 'RS256');
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ['RS256'], // algorithm is not trusted from the token
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
return payload;
}
The jose library is recommended for Node.js because it requires you to specify the algorithm independently of what the token claims.
Brute-Forcing Weak HMAC Secrets
HS256 tokens are signed with a shared secret. If the secret is weak — a dictionary word, a short string, a default value from a framework example — it can be brute-forced offline.
JWT cracking tools include hashcat (with the --hash-type 16500 mode for JWT) and jwt_tool. An attacker who captures a valid HS256 token from traffic or a data breach can crack a weak secret in seconds to minutes:
# hashcat JWT cracking
hashcat -a 0 -m 16500 jwt_token.txt wordlist.txt
# jwt_tool
python3 jwt_tool.py <token> -C -d wordlist.txt
Once the secret is known, the attacker can forge tokens with arbitrary claims.
Fix: Use a cryptographically random secret of at least 256 bits (32 bytes):
import crypto from 'crypto';
// Generate a strong secret (run once, store securely)
const secret = crypto.randomBytes(64).toString('hex');
// → 128 hex characters = 512 bits, far beyond brute-force feasibility
For production, store the secret in a secrets manager (AWS Secrets Manager, HashiCorp Vault) and never hardcode it. For high-security applications, switch to RS256 or ES256 (asymmetric algorithms) — there is no shared secret to steal or crack.
Claim Injection and Validation Failures
JWTs are self-contained: the payload carries all claims the server needs to make authorization decisions. If the server trusts claims without validating them properly, attackers can escalate privileges or hijack accounts.
Missing exp validation
Tokens without an expiration claim (exp) never expire. A token from a terminated employee or compromised session remains valid indefinitely unless explicitly revoked.
// Always validate expiration
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
// jwt library validates exp by default, but make it explicit
clockTolerance: 30, // allow 30 seconds of clock skew
});
// If exp is missing, reject
if (!decoded.exp) {
throw new Error('Token missing expiration');
}
Ignoring iss and aud
An application that accepts tokens signed by any issuer is vulnerable to token injection — an attacker uses a token issued by a different service that happens to use the same signing key, or exploits a key confusion between services in a multi-tenant system.
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'https://auth.example.com', // reject tokens from other issuers
audience: 'https://api.example.com', // reject tokens intended for other services
});
Privilege escalation via unvalidated claims
// Vulnerable: role is trusted directly from the token without server-side lookup
const { role } = jwt.verify(token, secret);
if (role === 'admin') grantAdminAccess();
// More robust: use the token only for identity, look up permissions from the database
const { sub } = jwt.verify(token, secret, { algorithms: ['HS256'] });
const user = await getUserById(sub);
if (user.role === 'admin') grantAdminAccess();
The JWT identifies who the user is. Authorization decisions should use server-side data where possible, especially for sensitive operations.
JWT Best Practices Summary
Algorithm:
- Use RS256 or ES256 for tokens that must be verified by multiple services.
- Use HS256 only when a single service both issues and verifies tokens.
- Always specify the algorithm during verification — never accept it from the token header.
- Explicitly reject
none.
Secret management:
- Use secrets of at least 256 bits generated by a CSPRNG.
- Store secrets in a secrets manager, not in source code or environment variable files committed to git.
- Rotate signing keys periodically and on suspected compromise.
Claims:
- Always set
expwith a short lifetime (15 minutes for access tokens). - Set
issandaudand verify both on every token. - Use
jti(JWT ID) for tokens that must be revocable — store used JTIs in a cache and reject duplicates.
Token handling:
- Store tokens in
httpOnlycookies, notlocalStorage. - Use short access token lifetimes with rotating refresh tokens.
- Implement token revocation for logout and account compromise scenarios.
The jose library (Node.js) and python-jose or PyJWT (Python) are well-maintained and implement these controls correctly. Avoid rolling your own JWT verification logic.