JSON Web Token Security: Common Vulnerabilities and Fixes
JWTs are the de facto standard for API authentication, but their apparent simplicity hides real security pitfalls. Misconfigured JWTs have led to authentication bypasses, privilege escalation, and data breaches. This guide covers the most critical vulnerabilities and how to prevent them.
JWT Structure Recap
A JWT consists of three Base64URL-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ.signature
- Header: Algorithm and token type
- Payload: Claims (user data, expiration, etc.)
- Signature: Cryptographic verification
Inspect JWT structure with our JWT Encoder/Decoder.
For a foundational understanding of JWT structure, see our JWT Tokens Explained guide.
Critical Vulnerabilities
1. Algorithm Confusion Attack
The most dangerous JWT vulnerability. If a server accepts the alg header from the token without validation, an attacker can:
Attack: Change the algorithm from RS256 (asymmetric) to HS256 (symmetric) and sign the forged token with the server's public key:
// Attacker's forged token
header: { "alg": "HS256", "typ": "JWT" }
payload: { "sub": "admin", "role": "superadmin" }
// Signed with the server's PUBLIC key as the HMAC secret
If the server verifies HS256 tokens using the public key as the secret, the forged token passes verification.
Fix: Always specify the expected algorithm explicitly:
// WRONG - accepts whatever algorithm the token specifies
jwt.verify(token, key);
// CORRECT - enforce specific algorithm
jwt.verify(token, key, { algorithms: ['RS256'] });
2. None Algorithm Attack
Some libraries accept "alg": "none" β a token with no signature:
// Forged token with no signature
header: { "alg": "none", "typ": "JWT" }
payload: { "sub": "admin", "role": "superadmin" }
signature: "" // empty
Fix: Never allow none algorithm in production. Explicitly whitelist allowed algorithms.
3. Weak Signing Secrets
HMAC-based JWTs (HS256/HS384/HS512) are only as strong as the secret:
// TERRIBLE - can be brute-forced in seconds
secret = "password123"
// WEAK - dictionary attack vulnerable
secret = "my-jwt-secret"
// STRONG - 256+ bits of randomness
secret = "a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0"
Fix: Use at least 256 bits of cryptographic randomness for HMAC secrets. Better yet, use asymmetric keys (RS256, ES256) where the signing key never needs to be shared.
4. Missing Expiration
Tokens without expiration never expire β a stolen token grants permanent access:
// WRONG - no expiration
{ "sub": "user123", "role": "admin" }
// CORRECT - short expiration
{
"sub": "user123",
"role": "admin",
"exp": 1705312800,
"iat": 1705309200,
"nbf": 1705309200
}
Best practice: Access tokens expire in 15-60 minutes. Use refresh tokens (stored securely) for long sessions.
5. Sensitive Data in Payload
JWT payloads are Base64URL-encoded, not encrypted. Anyone can decode them:
echo "eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ" | base64 -d
# {"sub":"123","role":"admin"}
Never include: Passwords, API keys, credit card numbers, personal data (SSN, medical records), or any secret in JWT payloads.
6. Token Storage (XSS vs CSRF)
| Storage | XSS Vulnerable | CSRF Vulnerable |
|---|---|---|
| localStorage | Yes | No |
| Cookie (no flags) | Yes | Yes |
| HttpOnly Cookie | No | Yes |
| HttpOnly + SameSite Cookie | No | No |
Recommended: Store JWTs in HttpOnly, Secure, SameSite=Strict cookies. This prevents JavaScript access (XSS defense) and cross-site requests (CSRF defense).
Set-Cookie: token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
Security Checklist
- Explicitly specify algorithms β never accept from token header
- Reject
nonealgorithm β always require a signature - Use strong secrets β 256+ bits for HMAC, 2048+ bits for RSA
- Set short expiration β 15-60 minutes for access tokens
- Validate all claims β
exp,iss,aud,nbf - Store in HttpOnly cookies β not localStorage
- Rotate secrets periodically β plan for key rotation
- Never store sensitive data in the payload
- Use asymmetric keys for distributed systems (RS256 or ES256)
- Implement token revocation β blacklist or short expiration + refresh tokens
Asymmetric vs Symmetric Signing
| Aspect | HS256 (Symmetric) | RS256 (Asymmetric) |
|---|---|---|
| Key | Shared secret | Private/public key pair |
| Who can sign | Anyone with the secret | Only the private key holder |
| Who can verify | Anyone with the secret | Anyone with the public key |
| Key distribution | Secret must be shared securely | Only public key is shared |
| Performance | Faster | Slower |
| Best for | Single service | Microservices, distributed systems |
Recommendation: Use ES256 (ECDSA) for new applications β it provides asymmetric security with performance close to HMAC.
Token Revocation Strategies
JWTs are stateless by design β there is no built-in way to revoke them. Strategies:
- Short expiration: If tokens expire in 15 minutes, a stolen token's window is limited
- Refresh token rotation: Issue a new refresh token with each use; if a refresh token is used twice, revoke all tokens
- Blacklist: Store revoked token IDs (jti claims) and check on each request
- Token versioning: Include a version number in claims; increment the user's version on logout
FAQ
Should I use JWT or session cookies for authentication?
Session cookies are simpler and more secure for traditional web applications β the server controls session lifecycle, and revocation is instant. JWTs are better for stateless APIs, microservices, and mobile apps where server-side session storage is impractical. If you choose JWTs, implement the security measures in this guide.
What is the ideal JWT expiration time?
For access tokens: 15-60 minutes. Shorter is more secure but requires more frequent refresh. For refresh tokens: 1-30 days, stored in HttpOnly cookies. For single-use tokens (email verification, password reset): 1-24 hours.
Related Resources
- JWT Encoder/Decoder β Inspect and decode JWTs safely
- JWT Tokens Explained β Understand JWT structure and flow
- Password Entropy Explained β Strength of JWT signing secrets