JWT Attacks: How JSON Web Tokens Are Exploited and How to Prevent It
JWT attacks are a category of authentication vulnerabilities that exploit weaknesses in how JSON Web Tokens are generated, signed, and validated. Because JWTs are used as authentication tokens in the vast majority of modern web APIs, a single JWT vulnerability can bypass all application-level authorization and grant attackers complete control over any account — including admin accounts. Understanding how these attacks work is essential for any developer or security professional building API-based systems.
JWT Structure Review
A JWT consists of three base64url-encoded parts separated by periods:
header.payload.signatureHeader (algorithm and token type):
{
"alg": "HS256",
"typ": "JWT"
}Payload (claims):
{
"sub": "user123",
"role": "user",
"exp": 1735689600
}Signature: HMAC-SHA256(base64url(header) + '.' + base64url(payload), secret)
The signature cryptographically binds the header and payload to the secret. Any modification to either part invalidates the signature — *if* the validation is implemented correctly.
Attack 1: The 'none' Algorithm Bypass
This is the most famous JWT vulnerability and should not exist in any modern library, yet it persists in custom implementations and some outdated libraries.
How It Works
The JWT spec includes "alg": "none" as a valid algorithm meaning 'unsigned token'. If a library accepts none as a valid algorithm when validating tokens, an attacker can create a forged token with any payload and no valid signature:
Original token header:
{"alg": "HS256", "typ": "JWT"}Forged token header:
{"alg": "none", "typ": "JWT"}Forged token (signature is empty or omitted):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.The payload sets role: admin. If the server validates this token and accepts none as an algorithm, authentication is completely bypassed.
Prevention
Never accept none as a valid algorithm. Explicitly whitelist the algorithms your application uses:
# PyJWT — CORRECT
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=['HS256'] # explicit whitelist
)
# WRONG — allows none bypass
payload = jwt.decode(token, SECRET_KEY)Attack 2: Algorithm Confusion (RS256 → HS256)
This is a more sophisticated and broadly impactful attack against systems using asymmetric algorithms (RS256, ES256).
How It Works
RSA-based JWT systems use a private key to sign tokens and a public key to verify them. The public key is often publicly accessible (JWKS endpoint, documentation, even embedded in client code).
The attack exploits the fact that some libraries — when verifying an HS256 token — use the same key for both signing and verification. If a server generates RS256 tokens but doesn't validate that incoming tokens use RS256, an attacker can:
- 1Download the server's RSA public key (often at
/.well-known/jwks.json) - 2Create a new token with
"alg": "HS256"in the header - 3Sign it with the RSA public key (used as the HMAC secret)
- 4Send it to the server
The server takes the public key (which it uses for RS256 verification), and uses it as the HMAC secret for HS256 verification — because that's what the token's alg header says to do. The signatures match.
Result: The attacker forges a valid token with any payload they choose — including "role": "admin".
Prevention
Never trust the algorithm specified in the JWT header. The server must enforce its expected algorithm:
# SAFE — algorithm enforced server-side
payload = jwt.decode(
token,
PUBLIC_KEY,
algorithms=['RS256'] # explicitly only RS256 is accepted
)// Node.js — SAFE
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, callback);The algorithm whitelist must be configured on the *verifier*, not derived from the token header.
Attack 3: Weak Secret Brute Force
HS256, HS384, and HS512 tokens are signed with a symmetric secret. If that secret is weak, it can be brute-forced offline.
How It Works
An attacker who obtains a JWT (from an API response, a logged request, a leak) can attempt to brute-force the signing secret offline. The token and signature are public. The only unknown is the secret.
Tools like hashcat and jwt_tool support offline JWT secret cracking:
hashcat -a 0 -m 16500 captured.jwt /usr/share/wordlists/rockyou.txtCommon weak secrets found in real applications:
- secret
- password
- jwt_secret
- Application name
- Default framework secrets left in place
- Short randomly-generated secrets (< 32 bytes)
Once the secret is found, the attacker can sign arbitrary tokens with any payload.
Prevention
- 1Use a cryptographically random secret of at least 256 bits (32 bytes)
- 2Rotate secrets periodically
- 3Store secrets in a secrets manager (AWS Secrets Manager, HashiCorp Vault), not in environment variables or code
import secrets
JWT_SECRET = secrets.token_hex(32) # 256-bit random secretFor high-security applications, prefer asymmetric algorithms (RS256, ES256) over symmetric ones. A compromised private key can be rotated; a compromised symmetric secret invalidates all existing tokens and requires immediate rotation.
Attack 4: kid (Key ID) Injection
The JWT kid (Key ID) header parameter is a hint to the server about which key to use for verification. If the server uses kid in an unsafe way, it becomes an injection vector.
SQL Injection via kid
If the server uses kid to look up the key in a database:
# VULNERABLE
key = db.query(f"SELECT key_value FROM jwt_keys WHERE id = '{kid}'")An attacker sets kid to a SQL injection payload:
{
"alg": "HS256",
"kid": "1' UNION SELECT 'attacker_controlled_secret' --"
}The query returns 'attacker_controlled_secret' as the key. The attacker then signs the forged token with that same string, and the signature is valid.
Path Traversal via kid
If the server reads the key from a file system path indicated by kid:
# VULNERABLE
key_path = f'/keys/{kid}'
with open(key_path, 'r') as f:
key = f.read()Attacker sets kid to ../../dev/null (or any known file), signs the token with the content of that file (e.g., empty string for /dev/null), and forges a valid token.
Prevention
- 1Validate
kidagainst a strict whitelist of expected key identifiers - 2Never use
kiddirectly in database queries or file paths - 3Use a hard-coded map of key IDs to key values rather than dynamic lookup
# SAFE
VALID_KEYS = {
'key-2024-01': 'secret_key_1',
'key-2024-02': 'secret_key_2',
}
def get_signing_key(kid):
key = VALID_KEYS.get(kid)
if not key:
raise InvalidTokenError('Unknown key ID')
return keyAttack 5: JWT Expiration and Revocation Issues
Ignoring Expiration
Some implementations decode and use JWT payloads without validating the exp (expiration) claim. Always validate expiration:
# PyJWT validates exp by default
# To check: ensure options={'verify_exp': True} is not disabledNo Revocation Mechanism
JWTs are stateless by design — the server doesn't store issued tokens. This means a compromised token remains valid until it expires. For security-sensitive applications, implement token revocation:
- 1Blocklist approach: Maintain a blocklist of revoked token JTI (JWT ID) claims. Check the blocklist on every request.
- 2Short expiration + refresh tokens: Issue short-lived access tokens (15 minutes) and longer-lived refresh tokens stored server-side. Revoke the refresh token to force re-authentication.
Complete JWT Security Checklist
- 1Explicitly whitelist accepted algorithms (
['HS256']or['RS256']— never both, nevernone) - 2Use a strong, random secret of at least 256 bits for HS256
- 3Prefer asymmetric algorithms (RS256, ES256) for multi-service architectures
- 4Validate all standard claims:
exp,iat,iss,aud - 5Validate and sanitize the
kidheader parameter against a whitelist - 6Implement token revocation for sensitive operations
- 7Use short expiration times (15 min for access tokens)
- 8Store JWT secrets in a secrets manager, not in code or
.envfiles - 9Rotate signing keys periodically
- 10Use a well-maintained JWT library — never implement JWT parsing from scratch
FAQ
Q: Is the 'none' algorithm bypass still relevant in modern libraries?
A: Most major JWT libraries have patched the none bypass. However, custom implementations, outdated library versions, and libraries that don't enforce algorithm whitelisting are still vulnerable. Always specify an explicit algorithm whitelist when decoding tokens, regardless of your library.
Q: How do I check if my application is vulnerable to algorithm confusion?
A: Obtain a valid JWT from your application. Change the alg header to HS256 (if it uses RS256). Get the application's public key (from JWKS endpoint or documentation). Sign the modified token with the public key as the HMAC secret. If the forged token is accepted, you're vulnerable. Tools like jwt_tool automate this test.
Q: Should I use JWT for session management?
A: JWT is best suited for stateless API authentication, service-to-service authentication, and short-lived access tokens. For traditional web session management, server-side sessions with opaque session tokens (stored in HttpOnly cookies) are simpler, more secure, and easier to revoke. JWT's statelessness is a trade-off: scalability vs. revocability.
Q: What is a JWKS endpoint and should I expose one?
A: JWKS (JSON Web Key Set) is a standard endpoint (usually /.well-known/jwks.json) that exposes your service's public keys for asymmetric JWT verification. It's standard practice for public-facing authentication servers (OAuth providers, etc.) and enables key rotation without coordination. If your JWT signing keys are truly public (RS256 public keys), a JWKS endpoint is appropriate. Never expose symmetric secrets.
Q: Can JWT attacks be detected in logs?
A: Yes. Log and monitor for: JWT decode failures (invalid signatures, expired tokens), unusual kid values, alg: none in token headers, tokens with impossible iat (issued in the future), and high volumes of failed authentications from the same IP. These are indicators of active JWT attack attempts.
Conclusion
JWT attacks span from embarrassingly simple (none algorithm bypass) to technically sophisticated (algorithm confusion, kid injection). The common thread is that most of these vulnerabilities come from trusting the token header rather than enforcing server-side policy. The fix is always the same direction: make your server explicit about what it accepts, use modern well-maintained libraries, and validate all claims rigorously.
Combined with a strong security configuration baseline — checked and maintained automatically — you create a defense-in-depth posture that makes your application significantly harder to compromise.