Vulnerability
Express

JWT Vulnerabilities in Express.js

Express.js applications overwhelmingly use the jsonwebtoken (jwt) package for token-based authentication. The most critical vulnerability is algorithm confusion — calling jwt.verify(token, secret) without specifying algorithms allows attackers to forge tokens. Combined with weak secrets, missing expiration checks, and tokens in Authorization headers exposed to XSS, JWT-based Express APIs are frequently compromised.

Scan Your Express App

How JWT Vulnerabilities Manifests in Express

The most dangerous JWT vulnerability in Express is algorithm confusion. The jsonwebtoken library's jwt.verify(token, secret) accepts whatever algorithm is in the token's header. An attacker changes the algorithm from RS256 to HS256 and signs with the server's public key (which is public), creating a valid token. Weak secrets are equally common. Developers use short, predictable strings like "secret" or "my-jwt-secret" that can be brute-forced. JWT cracking tools can test millions of secrets per second against a captured token. Missing expiration validation occurs when tokens are created without the exp claim or when verification does not check it. Tokens without expiration remain valid forever — even after a user changes their password or is deactivated. Express middleware that passes JWT payloads directly to route handlers without re-validating claims allows privilege escalation. An attacker modifies their token's role claim and if the server only checks the signature (not the claims), the escalation succeeds.

Real-World Impact

An Express API used jwt.verify(token, publicKey) for RS256 verification. An attacker changed the token header to HS256 and signed it with the public key as the HMAC secret. The server treated the public key as the HMAC key and accepted the forged token. The attacker gained admin access and exported the entire user database. Another Express app used the secret "development" for JWT signing. An attacker captured a valid token from an API response, cracked the secret using jwt-cracker in under a minute, and forged tokens for any user account.

Step-by-Step Fix

1

Fix algorithm confusion vulnerability

Always specify the expected algorithm when calling jwt.verify() to prevent algorithm switching attacks.

import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET; // At least 32 random bytes

// VULNERABLE - accepts any algorithm
const payload = jwt.verify(token, SECRET);

// SECURE - only accepts HS256
const payload = jwt.verify(token, SECRET, {
  algorithms: ['HS256'],
  issuer: 'your-app',
  audience: 'your-api',
});

// For RS256 with public/private key pair:
const publicKey = fs.readFileSync('public.pem');
const payload = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // CRITICAL: prevents HS256 downgrade
  issuer: 'your-app',
});
2

Create secure token generation

Generate tokens with proper claims including expiration, issuer, and audience.

import jwt from 'jsonwebtoken';
import crypto from 'crypto';

// Generate a strong secret (run once and store in env)
// const secret = crypto.randomBytes(32).toString('hex');

const SECRET = process.env.JWT_SECRET!;

function generateToken(user: { id: string; role: string }): string {
  return jwt.sign(
    {
      sub: user.id,
      role: user.role,
    },
    SECRET,
    {
      algorithm: 'HS256',
      expiresIn: '1h',
      issuer: 'your-app',
      audience: 'your-api',
    }
  );
}

function generateRefreshToken(userId: string): string {
  return jwt.sign({ sub: userId, type: 'refresh' }, SECRET, {
    algorithm: 'HS256',
    expiresIn: '7d',
  });
}
3

Use express-jwt middleware for route protection

The express-jwt middleware handles algorithm specification and token validation correctly.

import { expressjwt } from 'express-jwt';
import express from 'express';

const app = express();

// Protect routes with express-jwt
app.use(
  '/api',
  expressjwt({
    secret: process.env.JWT_SECRET!,
    algorithms: ['HS256'],
    issuer: 'your-app',
    audience: 'your-api',
  })
);

// Access authenticated user in route handlers
app.get('/api/profile', (req, res) => {
  // req.auth contains the verified JWT payload
  const userId = req.auth?.sub;
  // ... fetch user data
});

// Handle JWT errors
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
  if (err.name === 'UnauthorizedError') {
    res.status(401).json({ error: 'Invalid or expired token' });
  } else {
    next(err);
  }
});
4

Implement token refresh rotation

Use short-lived access tokens with refresh token rotation to limit exposure.

app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  try {
    const payload = jwt.verify(refreshToken, process.env.JWT_SECRET!, {
      algorithms: ['HS256'],
    }) as { sub: string; type: string };
    if (payload.type !== 'refresh') {
      return res.status(401).json({ error: 'Invalid token type' });
    }
    // Check if refresh token is in the database (not revoked)
    const stored = await db.refreshTokens.findOne({
      token: refreshToken, userId: payload.sub,
    });
    if (!stored) {
      return res.status(401).json({ error: 'Token revoked' });
    }
    // Rotate: delete old, create new
    await db.refreshTokens.delete({ token: refreshToken });
    const newAccess = generateToken({ id: payload.sub, role: stored.role });
    const newRefresh = generateRefreshToken(payload.sub);
    await db.refreshTokens.create({ token: newRefresh, userId: payload.sub });
    res.json({ accessToken: newAccess, refreshToken: newRefresh });
  } catch {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Prevention Best Practices

1. Always specify algorithms in jwt.verify(): jwt.verify(token, secret, { algorithms: ['HS256'] }). 2. Use a strong random secret of at least 256 bits (32 bytes). 3. Always set and validate token expiration (exp claim). 4. Validate issuer (iss) and audience (aud) claims. 5. Use express-jwt middleware which handles algorithm specification correctly. 6. Implement token refresh rotation to limit the impact of stolen tokens.

How to Test

1. Search your codebase for jwt.verify() calls without an algorithms option. 2. Decode a valid token, change "alg" to "none", remove the signature, and test if the server accepts it. 3. Check if your JWT secret is shorter than 32 characters or uses a common word. 4. Generate a token without exp claim and test if the server accepts it after a long time. 5. Use Vibe App Scanner to detect JWT configuration issues in your Express application.

Frequently Asked Questions

What is JWT algorithm confusion and how does it work?

Algorithm confusion exploits the jwt.verify() function when it does not enforce a specific algorithm. The attacker takes a token signed with RS256, changes the header to HS256, and signs it with the server's RSA public key (which is public). Since jwt.verify() uses the provided "secret" parameter as the HMAC key when the token says HS256, and the public key is passed as the secret, the forged signature validates. Always specify algorithms: ['RS256'] or algorithms: ['HS256'] explicitly.

How long should my JWT secret be?

For HS256, use at least 256 bits (32 bytes) of cryptographically random data. Generate it with crypto.randomBytes(32).toString('hex') for a 64-character hex string. Never use human-readable passwords or short strings. For RS256, use at least a 2048-bit RSA key pair.

Should I use JWTs or sessions for Express authentication?

Sessions stored server-side (in Redis or a database) are simpler to secure: you can revoke them instantly, they do not expose claims to the client, and there is no algorithm confusion risk. JWTs are better for stateless architectures and microservices where centralized session storage is impractical. If you use JWTs, implement refresh token rotation and keep access tokens short-lived.

Is Your Express App Vulnerable to JWT Vulnerabilities?

VAS automatically scans for jwt vulnerabilities vulnerabilities in Express applications and provides step-by-step remediation guidance with code examples.

Scans from $5, results in minutes. Get actionable fixes tailored to your Express stack.