06Auth
JWT Security
JWT Security — Instruction 06
Coverage
CWE-347, CWE-327 — JWT-specific attacks OWASP A07:2021, ASVS V3.5
JWT Attack Checks
1. Algorithm Confusion (None Attack)
// 🔴 CRITICAL — Accepts "alg: none" = no signature
jwt.verify(token, secret) // if library allows alg:none
// Attacker forges: { "alg": "none", "typ": "JWT" }
// Payload: { "role": "admin" }
// → No signature needed!
// 🟢 Always specify allowed algorithms explicitly
jwt.verify(token, secret, { algorithms: ['HS256'] })
jwt.verify(token, publicKey, { algorithms: ['RS256'] })
// Never allow: 'none', 'HS1', weak algorithms
2. Algorithm Confusion (HS256 vs RS256)
// 🔴 CRITICAL — If server uses RS256 but accepts HS256
// Attacker uses the PUBLIC KEY as the HS256 secret key!
// Public keys are... public → attacker can sign tokens
// 🟢 Lock algorithm to what the server expects
jwt.verify(token, publicKey, { algorithms: ['RS256'] })
// Never: jwt.verify(token, publicKey) // accepts any algorithm
3. jku Header Injection
// 🔴 CRITICAL — Server fetches keys from URL in token header
// Token header: { "alg": "RS256", "jku": "https://evil.com/keys.json" }
// Server fetches attacker's keys → validates attacker's token!
// 🟢 Never use jku automatically. If needed, validate against allowlist:
const ALLOWED_JKU = ['https://auth.yourdomain.com/.well-known/jwks.json']
if (!ALLOWED_JKU.includes(header.jku)) throw new Error('Invalid jku')
4. Embedded JWK Attack
// 🔴 CRITICAL — Token contains its own public key
// Header: { "alg": "RS256", "jwk": { "kty": "RSA", "n": "attacker_key" } }
// Server uses embedded key → validates attacker's own tokens
// 🟢 Never use key from token header. Use server-stored keys only.
5. kid (Key ID) Injection
// 🔴 SQL injection via kid header
// kid: "' UNION SELECT 'attacker_key'--"
// Server queries: SELECT key FROM keys WHERE id = '{kid}'
// → Attacker controls what key is used
// 🔴 Path traversal via kid
// kid: "../../dev/null" → empty key → signature verification passes
// 🟢 Validate kid against known keys only
const VALID_KIDS = ['key-v1', 'key-v2']
if (!VALID_KIDS.includes(header.kid)) throw new Error('Invalid kid')
6. Weak Secret
// 🔴 Short or common secrets are brute-forceable
JWT_SECRET=secret
JWT_SECRET=password
JWT_SECRET=12345678
JWT_SECRET=your-secret-key
// 🟢 Minimum 256-bit random secret
// Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
JWT_SECRET=a4b8c2d9e1f3a7b5c4d2e8f1a3b7c5d9e2f4a8b6c3d1e7f5a4b9c2d8e1f3a7b
7. Expiration Time
// 🔴 No expiration = tokens valid forever
jwt.sign(payload, secret) // no expiresIn
// 🔴 Too long
jwt.sign(payload, secret, { expiresIn: '365d' }) // 1 year!
// 🟢 Short-lived access tokens, refresh token pattern
jwt.sign(payload, secret, { expiresIn: '15m' }) // access token
jwt.sign(payload, refreshSecret, { expiresIn: '7d' }) // refresh token
8. Sensitive Data in Payload
// 🔴 JWT payload is Base64 encoded — NOT encrypted, readable by anyone!
jwt.sign({
userId: user.id,
password: user.password, // 🔴 NEVER
creditCard: user.card, // 🔴 NEVER
email: user.email // 🟡 Only if necessary
})
// 🟢 Minimal payload
jwt.sign({ sub: user.id, role: user.role }, secret)
9. Token Revocation
// 🔴 No revocation = stolen tokens are valid until expiry
// If user logs out or token is stolen, you can't invalidate it
// 🟢 Implement revocation (at least for critical tokens)
// Option 1: Short expiry (15min) + refresh tokens
// Option 2: Token blacklist (Redis)
const blacklist = new Set()
// On logout:
blacklist.add(token)
// On verify:
if (blacklist.has(token)) throw new Error('Token revoked')
10. JWT in localStorage vs Cookie
// 🔴 localStorage is accessible via XSS
localStorage.setItem('token', jwt)
// 🟢 HttpOnly cookie (not accessible via JS)
res.cookie('token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 min
})
11. Cross-JWT Confusion
// 🔴 Using tokens interchangeably between services
// Token for service A accepted by service B
// 🟢 Always verify audience (aud) claim
jwt.verify(token, secret, {
algorithms: ['RS256'],
audience: 'https://api.myservice.com', // reject tokens for other services
issuer: 'https://auth.myservice.com'
})
Complete JWT Verification Template
import jwt from 'jsonwebtoken'
function verifyToken(token) {
if (!token || typeof token !== 'string') throw new Error('Invalid token')
const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
algorithms: ['RS256'], // explicit algorithm
audience: process.env.JWT_AUDIENCE,
issuer: process.env.JWT_ISSUER,
clockTolerance: 30, // 30 seconds max clock drift
ignoreExpiration: false // always check expiry
})
// Check token is not revoked
if (isRevoked(decoded.jti)) throw new Error('Token revoked')
return decoded
}