Introduction
JSON Web Tokens (JWT) have become the de facto standard for stateless authentication in modern web applications. However, their security depends entirely on proper implementation. A recently patched vulnerability in an application using the jsonwebtoken library (^9.0.2) highlights a common but dangerous oversight: failing to specify allowed algorithms during token verification.
This vulnerability, while seemingly minor in code terms, can have catastrophic consequences—allowing attackers to completely bypass authentication and impersonate any user in your system. If you're using JWTs in your application, this is a must-read.
The Vulnerability Explained
What is Algorithm Confusion?
JWT tokens consist of three parts: a header, a payload, and a signature. The header specifies which cryptographic algorithm was used to sign the token. When verifying a JWT, the server must check both the signature's validity AND ensure the algorithm used is one that's expected and secure.
The vulnerability occurs when jwt.verify() is called without the algorithms parameter:
// VULNERABLE CODE
const decoded = jwt.verify(token, secretOrPublicKey);
Without explicit algorithm specification, the library will trust whatever algorithm is declared in the token's header. This opens two critical attack vectors:
Attack Vector 1: The "None" Algorithm Attack
An attacker can modify a valid JWT's header to use the "none" algorithm, which indicates no signature verification is required:
{
"alg": "none",
"typ": "JWT"
}
When the server attempts to verify this token without algorithm restrictions, it may accept the token as valid without checking any signature at all. The attacker simply needs to:
- Decode an existing JWT
- Change the algorithm header to "none"
- Remove the signature portion
- Re-encode the token
Result: Complete authentication bypass.
Attack Vector 2: RS256 to HS256 Confusion
Many applications use asymmetric algorithms (like RS256) where tokens are signed with a private key and verified with a public key. If algorithm validation is missing, an attacker can:
- Obtain the public key (often publicly accessible)
- Create a new token signed with HS256 (symmetric algorithm)
- Use the public key as the "secret" for HMAC signing
The server, expecting RS256 but not enforcing it, will use the public key to verify the HMAC signature—which the attacker created using that same public key.
Result: Token forgery with publicly available information.
Real-World Impact
This vulnerability allows attackers to:
- Impersonate any user by forging tokens with arbitrary user IDs
- Escalate privileges by adding admin roles to forged tokens
- Bypass authentication entirely using the "none" algorithm
- Access sensitive data belonging to other users
- Perform unauthorized actions on behalf of legitimate users
Example Attack Scenario
Consider an e-commerce application:
- Alice registers as a regular user and receives a JWT
- Attacker intercepts or obtains Alice's token
- Attacker decodes the token and changes the header to
"alg": "none" - Attacker modifies the payload:
{"userId": "admin", "role": "administrator"} - Attacker re-encodes without a signature
- Server verifies token without algorithm check and grants admin access
The attacker now has full administrative control over the application.
The Fix
What Changed
The fix involves adding the algorithms parameter to all jwt.verify() calls throughout the application:
// BEFORE (Vulnerable)
const decoded = jwt.verify(token, secretOrPublicKey);
// AFTER (Secure)
const decoded = jwt.verify(token, secretOrPublicKey, {
algorithms: ['RS256'] // Explicitly whitelist allowed algorithms
});
How It Solves the Problem
By specifying algorithms: ['RS256'] (or whichever algorithm your application uses), the verification process now:
- Rejects "none" algorithm tokens immediately, preventing signature bypass
- Prevents algorithm confusion by refusing tokens signed with unexpected algorithms
- Enforces cryptographic integrity by ensuring only your chosen secure algorithm is accepted
If an attacker tries to submit a token with "alg": "none" or "alg": "HS256" when only RS256 is allowed, the verification will fail with an error:
JsonWebTokenError: invalid algorithm
Complete Implementation Example
Here's a comprehensive example of secure JWT verification:
const jwt = require('jsonwebtoken');
const fs = require('fs');
// Load your keys securely (example for RS256)
const publicKey = fs.readFileSync('public-key.pem');
function verifyToken(token) {
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Whitelist allowed algorithms
issuer: 'your-app.com', // Verify token issuer
audience: 'your-api', // Verify intended audience
maxAge: '1h' // Enforce expiration
});
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token has expired');
}
if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
}
throw error;
}
}
// Express middleware example
function authenticateJWT(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
req.user = verifyToken(token);
next();
} catch (error) {
return res.status(403).json({ error: error.message });
}
}
Prevention & Best Practices
1. Always Specify Allowed Algorithms
Never call jwt.verify() without the algorithms parameter:
// ❌ NEVER DO THIS
jwt.verify(token, secret);
// ✅ ALWAYS DO THIS
jwt.verify(token, secret, { algorithms: ['HS256'] });
2. Use Strong, Appropriate Algorithms
- HS256/HS512: Suitable for symmetric scenarios where both signing and verification happen on the same server
- RS256/RS512: Recommended for distributed systems where tokens are signed by one service and verified by multiple services
- ES256/ES512: Modern elliptic curve algorithms offering better performance with equivalent security
Avoid: HS256 with RS256 public keys, any "none" algorithm, or weak algorithms like HS1.
3. Implement Defense in Depth
Algorithm specification is just one layer. Also implement:
const decoded = jwt.verify(token, secret, {
algorithms: ['RS256'],
issuer: 'trusted-issuer', // Verify token source
audience: 'your-service', // Verify token destination
maxAge: '15m', // Short expiration
clockTolerance: 30 // Allow 30s clock skew
});
// Additional validation
if (!decoded.sub || !decoded.role) {
throw new Error('Invalid token structure');
}
4. Use Security Linting Tools
Configure ESLint with security plugins to catch these issues:
{
"plugins": ["security"],
"rules": {
"security/detect-unsafe-jwt": "error"
}
}
5. Regular Security Audits
- Automated scanning: Use tools like
npm audit, Snyk, or OWASP Dependency-Check - Code review: Ensure all JWT operations are reviewed by security-aware developers
- Penetration testing: Include JWT attack vectors in security assessments
6. Keep Dependencies Updated
The jsonwebtoken library has evolved to address security concerns. Always use the latest stable version:
npm update jsonwebtoken
npm audit fix
Security Standards References
- CWE-347: Improper Verification of Cryptographic Signature
- CWE-327: Use of a Broken or Risky Cryptographic Algorithm
- OWASP Top 10 2021: A02:2021 – Cryptographic Failures
- OWASP JWT Cheat Sheet: Comprehensive guide to JWT security
- RFC 7519: JSON Web Token specification
Conclusion
The JWT algorithm confusion vulnerability demonstrates that security often fails not from complex exploits, but from simple oversights in implementation. A single missing parameter—algorithms—can completely undermine your authentication system.
Key Takeaways:
- Always specify allowed algorithms when verifying JWTs
- Use asymmetric algorithms (RS256) for distributed systems
- Implement multiple layers of validation beyond just signature verification
- Keep security libraries updated and monitor for vulnerabilities
- Audit your codebase specifically for JWT verification calls
If you're using the jsonwebtoken library, review your codebase today. Search for all instances of jwt.verify() and ensure they include the algorithms parameter. This simple fix could be the difference between a secure application and a compromised one.
Remember: in security, what you don't specify can hurt you as much as what you do. Write defensive code, assume malicious input, and always validate explicitly.
Have you checked your JWT implementation? Share your experiences or questions in the comments below.
Stay secure, and happy coding!