JWT Security: Algorithm Confusion, Weak Secrets, and the Vulnerabilities Still Hitting Production

JSON Web Tokens are the dominant stateless authentication mechanism for APIs and SPAs — and they carry a well-documented set of implementation vulnerabilities that remain common in production systems. This guide covers the attack classes, vulnerable code patterns, and secure implementations.

Why JWTs Are Still Getting Exploited

JSON Web Tokens have been the standard mechanism for stateless API authentication for over a decade. The specification is well-understood, the libraries are mature, and the vulnerabilities are thoroughly documented. And yet JWT implementation flaws continue to appear in bug bounty reports, pentests, and breach post-mortems with striking regularity.

The reason is that JWT’s flexibility — which is a genuine design feature — creates a surface area where subtle misconfigurations have high-severity consequences. The algorithm is specified by the token itself. The signature can be absent. The secret can be weak and guessable. Each of these properties, if a library handles them naively, enables authentication bypass.

This guide covers the four most common JWT vulnerability classes, with vulnerable and fixed code examples in Python, JavaScript, and Java.

Vulnerability 1: The none Algorithm Attack

The JWT specification allows "alg": "none" as a valid algorithm value, indicating an unsigned token. Some libraries, particularly older versions, honour this without checking whether the application actually intends to accept unsigned tokens.

Attack: Strip the signature from a valid JWT, change the alg header to "none", and present it. If the library accepts it, the attacker controls all token claims.

Vulnerable Python (PyJWT <2.0):

import jwt

# Attacker-crafted token with alg: none
token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9."

# Old PyJWT accepted this without error
decoded = jwt.decode(token, options={"verify_signature": False})
# decoded = {"sub": "admin", "role": "admin"}

Secure Python:

import jwt

def verify_token(token: str, secret: str) -> dict:
    try:
        # Explicitly specify allowed algorithms — NEVER pass algorithms=None
        payload = jwt.decode(
            token,
            secret,
            algorithms=["HS256"],  # Whitelist only
            options={"verify_exp": True}
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise AuthError("Token expired")
    except jwt.InvalidTokenError as e:
        raise AuthError(f"Invalid token: {e}")

Never pass algorithms=None or allow the library to infer the algorithm from the token header.

Vulnerability 2: Algorithm Confusion (RS256 → HS256)

This is the most sophisticated and highest-impact JWT attack. It exploits libraries that accept both asymmetric (RS256) and symmetric (HS256) algorithms on the same endpoint.

The attack:

  1. The server uses RS256 — tokens are signed with the private key, verified with the public key
  2. The attacker obtains the public key (often published at a JWKS endpoint)
  3. The attacker crafts a token with "alg": "HS256" and signs it with the public key as the HMAC secret
  4. A naive library sees alg: HS256, treats the verification key as the HMAC secret, and verifies the signature using the (known) public key — which matches the attacker’s signature
  5. Authentication bypassed

Vulnerable Node.js pattern:

const jwt = require('jsonwebtoken');

// Dangerous: letting the library use whatever algorithm the token specifies
app.post('/verify', (req, res) => {
  const token = req.headers.authorization.split(' ')[1];
  
  // VULNERABLE: publicKey used as secret when alg=HS256 in token header
  const decoded = jwt.verify(token, publicKey); // No algorithms restriction
  res.json({ user: decoded.sub });
});

Secure Node.js:

const jwt = require('jsonwebtoken');

const ALLOWED_ALGORITHMS = ['RS256']; // Never include HS256 if using RSA

app.post('/verify', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });

  try {
    const decoded = jwt.verify(token, publicKey, {
      algorithms: ALLOWED_ALGORITHMS, // Hard-coded algorithm whitelist
      issuer: 'https://auth.example.com',
      audience: 'api.example.com',
    });
    res.json({ user: decoded.sub });
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

The fix is always the same: hard-code the expected algorithm on the verification side. Never trust the alg header in the token.

Vulnerability 3: Weak HMAC Secrets

HS256-signed tokens are only as secure as the secret used to sign them. Common failures:

  • Default secrets from documentation or tutorials (secret, your-256-bit-secret, application name)
  • Short secrets (under 256 bits for HS256)
  • Environment variable defaults not overridden in production
  • Secrets committed to version control

Detection via offline cracking:

Tools like hashcat and jwt_tool can crack weak JWT secrets offline:

# jwt_tool brute force
python3 jwt_tool.py <token> -C -d /usr/share/wordlists/rockyou.txt

# hashcat mode 16500 (JWT HS256/HS384/HS512)
hashcat -a 0 -m 16500 <token> /usr/share/wordlists/rockyou.txt

If a secret can be cracked offline, an attacker can forge arbitrary tokens for any user.

Secure secret generation:

import secrets
import base64

# Generate a cryptographically random 256-bit (32-byte) secret
secret = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
print(secret)  # Store this in a secrets manager — not in code or .env files
// Node.js
const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('base64url');

For production applications, store JWT secrets in AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, or HashiCorp Vault — not in environment variables or configuration files.

Vulnerability 4: Missing Claim Validation

Even with a correctly verified signature, JWTs can be misused if claim validation is incomplete.

Common failures:

# VULNERABLE: signature verified but no claim validation
payload = jwt.decode(token, secret, algorithms=["HS256"])
# If token was issued for a different service, an attacker can replay it here

# SECURE: validate all relevant claims
payload = jwt.decode(
    token,
    secret,
    algorithms=["HS256"],
    options={
        "verify_exp": True,      # Token hasn't expired
        "verify_nbf": True,      # Token is valid now (not before)
        "verify_iat": True,      # Issued-at is reasonable
        "verify_aud": True,      # Token was issued for this audience
        "verify_iss": True,      # Token was issued by trusted issuer
    },
    audience="api.example.com",
    issuer="https://auth.example.com",
)

Java (Spring Security / JJWT):

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.security.Key;

public Claims validateToken(String token) {
    Key signingKey = Keys.hmacShaKeyFor(secretBytes); // 256+ bit secret
    
    return Jwts.parserBuilder()
        .setSigningKey(signingKey)
        .requireIssuer("https://auth.example.com")
        .requireAudience("api.example.com")
        // JJWT validates exp automatically when present
        .build()
        .parseClaimsJws(token)  // Throws if invalid — never use parseClaimsJwt()
        .getBody();
}

Note: use parseClaimsJws() (which validates signature) never parseClaimsJwt() (which does not).

Secure JWT Checklist

  • Algorithm is hard-coded on the server side — never read from the token header
  • none algorithm is explicitly rejected
  • HMAC secrets are at least 256 bits, randomly generated, stored in a secrets manager
  • RS256/ES256 public keys cannot be used as HS256 secrets (enforce algorithm type at the verification layer)
  • exp, iss, aud claims are validated on every request
  • Short token lifetimes (15 minutes for access tokens) with refresh token rotation
  • JWTs are not stored in localStorage (prefer httpOnly cookies for browser-facing applications)
  • Token revocation strategy exists (JTI blocklist, short expiry, or refresh token rotation)

Testing Your Implementation

jwt_tool covers all the attack classes above:

# Install
pip install jwt_tool

# Test all JWT attacks against a live endpoint
python3 jwt_tool.py <token> -t https://api.example.com/protected \
  -rh "Authorization: Bearer *JWT*" \
  -M at  # All tests

The none algorithm, RS256→HS256 confusion, and weak secret attacks all have automated test modes in jwt_tool. Run these against your authentication endpoints as part of your security testing pipeline.