JWT Simple No Verification

Critical Risk Authentication Bypass
jwtauthenticationverificationjwt-simplenodejstoken-forgery

What it is

The Node.js application uses the jwt-simple library to decode JWT tokens without proper signature verification, allowing attackers to forge or tamper with tokens. This vulnerability can lead to authentication bypass, privilege escalation, and unauthorized access to protected resources.

// Vulnerable: JWT decoding without verification
const jwt = require('jwt-simple');

app.get('/protected', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  try {
    // Dangerous: No signature verification
    const payload = jwt.decode(token, null, true); // noVerify = true
    
    // Trust unverified token data
    req.user = payload;
    res.json({ message: 'Access granted', user: payload });
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});
// Secure: Proper JWT verification
const jwt = require('jwt-simple');

const JWT_SECRET = process.env.JWT_SECRET;
const ALGORITHM = 'HS256';

if (!JWT_SECRET) {
  throw new Error('JWT_SECRET environment variable required');
}

app.get('/protected', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    // Secure: Verify signature with secret and algorithm
    const payload = jwt.decode(token, JWT_SECRET, false, ALGORITHM);
    
    // Validate token expiration
    if (payload.exp && Date.now() >= payload.exp * 1000) {
      return res.status(401).json({ error: 'Token expired' });
    }
    
    req.user = payload;
    res.json({ message: 'Access granted', userId: payload.sub });
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Applications use jwt-simple's decode() function without providing secret key for signature verification: jwt.decode(token, null) or jwt.decode(token, '', null, true) with noVerify flag. Developers misunderstand JWT security model thinking decode() validates tokens when it only parses them. Code extracts user information from JWT payload without verifying the token was actually signed by trusted authority. Attackers craft arbitrary JWT tokens with forged claims (userId, role, permissions) knowing application won't verify signature. Applications trust token contents implicitly allowing complete authentication bypass. This is especially dangerous when JWT contains authorization data like admin flags or user roles that control access to sensitive functionality.

Root causes

Using jwt.decode() Without Signature Verification

Applications use jwt-simple's decode() function without providing secret key for signature verification: jwt.decode(token, null) or jwt.decode(token, '', null, true) with noVerify flag. Developers misunderstand JWT security model thinking decode() validates tokens when it only parses them. Code extracts user information from JWT payload without verifying the token was actually signed by trusted authority. Attackers craft arbitrary JWT tokens with forged claims (userId, role, permissions) knowing application won't verify signature. Applications trust token contents implicitly allowing complete authentication bypass. This is especially dangerous when JWT contains authorization data like admin flags or user roles that control access to sensitive functionality.

Missing or Null Secret Keys for JWT Verification

JWT verification configured with null, undefined, empty string, or placeholder secret keys: jwt.decode(token, process.env.JWT_SECRET) when JWT_SECRET environment variable not set. Applications deployed without proper secret key configuration due to missing environment variables. Development uses weak secrets like 'secret', 'test123', or 'changeme' that get deployed to production. Secret key stored in code comments, documentation, or committed to version control. Teams don't implement secret rotation meaning compromised keys persist indefinitely. No validation at startup ensuring JWT_SECRET exists and meets minimum complexity requirements. Null/empty secrets cause jwt-simple to skip verification entirely allowing any token to pass validation.

Disabling Signature Verification for Debugging

Developers disable JWT signature verification during development for convenience testing with arbitrary tokens: jwt.decode(token, null, true) with noVerify parameter. Conditional logic checks environment but defaults to insecure when NODE_ENV undefined or misconfigured: const secret = isDev ? null : process.env.JWT_SECRET. Debug flags or feature toggles disable verification: if (!config.VERIFY_JWT) return jwt.decode(token, null, true). These debugging conveniences get accidentally deployed to production or left enabled after testing. No code review catches disabled verification because it's buried in utility functions. Monitoring doesn't detect unverified token usage. Penetration testing discovers authentication bypass but only after deployment.

Improper Algorithm Specification Handling

Applications don't specify expected JWT signing algorithm in decode() call allowing attackers to control algorithm through token header: jwt.decode(token, secret) accepts any algorithm including 'none'. JWT tokens can specify 'none' algorithm in header indicating no signature required - library processes these without verification even when secret provided. Applications vulnerable to algorithm confusion attacks where attacker changes RS256 (asymmetric) to HS256 (symmetric) and signs with server's public key used as HMAC secret. Code doesn't validate alg claim matches expected algorithm before verification. No allowlist of permitted algorithms means attackers can downgrade to weak algorithms (HS256 with short secrets) or exploit algorithm-specific vulnerabilities.

Using jwt-simple in Insecure Decode-Only Mode

Applications use jwt-simple purely for token parsing without understanding security implications. Code treats JWT as simple base64-encoded data container rather than cryptographically-signed authentication token. Developers copy-paste code examples from Stack Overflow or outdated tutorials showing insecure usage patterns. Teams choose jwt-simple for its simplicity without evaluating security features compared to alternatives like jsonwebtoken. Documentation confusion where developers don't understand difference between decode (no verification) and verify (with signature check). Legacy code uses jwt-simple before better libraries existed and was never updated. No security reviews of authentication code to identify missing signature verification. Applications validate other aspects (expiration, issuer) but skip signature verification undermining entire JWT security model.

Fixes

1

Always Use jwt.decode() with Proper Secret and Algorithm

Never call jwt.decode() without providing secret key and algorithm parameters: const payload = jwt.decode(token, process.env.JWT_SECRET, false, 'HS256'). The third parameter (noVerify) must be false (default) to enable signature verification. Fourth parameter specifies expected algorithm preventing algorithm confusion attacks. Validate JWT_SECRET environment variable exists and meets complexity requirements at application startup. Use different secrets for different environments and token types (access tokens, refresh tokens). Generate secrets using cryptographically secure random generators: crypto.randomBytes(64).toString('hex'). Never hardcode secrets in source code. Implement secret rotation procedures allowing graceful transition to new keys without invalidating all existing tokens simultaneously using key versioning in JWT kid header.

2

Implement Robust Secret Key Management

Store JWT secrets in secure secret management systems like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or Kubernetes Secrets rather than environment variables. Use secret rotation schedules (quarterly or on-demand after suspected compromise) with overlap periods supporting both old and new secrets during transition. Implement key versioning using JWT kid (Key ID) header claim allowing multiple valid signing keys simultaneously. For RSA/ECDSA algorithms, use public/private key pairs with private keys stored securely and never exposed to application code. Generate strong secrets: minimum 256 bits (32 bytes) for HS256, 384 bits (48 bytes) for HS384, 512 bits (64 bytes) for HS512. Use separate secrets per microservice or tenant in multi-tenant applications. Monitor secret access and log all JWT verification failures for security analysis.

3

Specify Explicit Algorithm and Validate Algorithm Claim

Always specify expected algorithm explicitly in jwt.decode() call: jwt.decode(token, secret, false, 'HS256') preventing algorithm confusion attacks. Validate algorithm claim in JWT header matches expected value before verification. Reject tokens with 'none' algorithm: const decoded = jwt.decode(token, null, true); if (decoded.header.alg === 'none') throw new Error('Algorithm none not permitted'). For asymmetric algorithms (RS256, ES256), ensure you're using public key for verification not private key. Create allowlist of permitted algorithms based on your token issuance: const ALLOWED_ALGS = ['HS256', 'RS256']; if (!ALLOWED_ALGS.includes(alg)) reject. Never let clients control algorithm selection. Document which algorithms your application uses and why in security documentation.

4

Validate Token Expiration and Standard Claims

After signature verification, validate standard JWT claims including expiration (exp), not-before (nbf), issued-at (iat), and audience (aud). Check exp claim: if (payload.exp < Date.now() / 1000) throw new Error('Token expired'). Validate aud claim matches expected audience for your application. Check iss (issuer) claim matches expected token issuer. Implement nbf (not before) validation for tokens that shouldn't be used until future time. Enforce reasonable token lifetimes: access tokens expire in minutes/hours (15 minutes to 1 hour), refresh tokens in days/weeks. Implement token revocation list or use short-lived tokens eliminating need for revocation. Add custom claims validation for authorization data (roles, permissions) ensuring they meet expected format and values.

5

Migrate to jsonwebtoken Library for Enhanced Security

Replace jwt-simple with the more feature-rich and actively maintained jsonwebtoken library providing better security defaults. Install jsonwebtoken: npm install jsonwebtoken and replace jwt-simple imports. Use jwt.verify() which enforces signature verification: const payload = jwt.verify(token, secret, {algorithms: ['HS256']}). jsonwebtoken provides automatic expiration validation, algorithm allowlist enforcement, and better error handling. Configure verification options: {algorithms: ['HS256'], issuer: 'your-app', audience: 'api'}. jsonwebtoken actively maintained with security updates while jwt-simple has limited maintenance. Migration requires updating all jwt.encode/decode calls to jwt.sign/verify with updated options object. Test thoroughly ensuring token generation and validation work correctly after migration.

6

Implement Comprehensive Error Handling for Token Validation

Wrap all JWT verification in try-catch blocks handling specific error types: try { const payload = jwt.decode(token, secret, false, 'HS256'); } catch (err) { if (err.message === 'Signature verification failed') return 401; if (err.message === 'Token expired') return 401; throw err; }. Log verification failures with context (user ID, IP address, timestamp, error type) for security monitoring. Return generic error messages to clients (e.g., 'Invalid token') without revealing specific validation failures that could aid attackers. Implement rate limiting on authentication endpoints to prevent brute-force token guessing. Monitor for unusual patterns: many expired tokens from same user (possible replay attacks), signature failures (token tampering attempts). Alert security team on sustained verification failures indicating potential attack. Never return decoded token contents in error responses revealing information to unauthorized users.

Detect This Vulnerability in Your Code

Sourcery automatically identifies jwt simple no verification and many other security issues in your codebase.