What is an Insecure JWT Implementation?
JSON Web Tokens (JWTs) are widely used for stateless authentication. A JWT consists of three base64url-encoded parts: header, payload, and signature. The signature ensures the token has not been tampered with. When JWT libraries or application code are misconfigured, attackers can forge arbitrary tokens and impersonate any user — including administrators.
The most critical vulnerabilities include: accepting the none algorithm (no signature required), algorithm confusion attacks (swapping RS256 for HS256 using the public key as secret), using weak or default HMAC secrets, and trusting the kid (Key ID) header for key selection without validation.
How exploitation works
The “none” algorithm attack
A valid JWT header specifies "alg": "RS256". An attacker changes it to "alg": "none", removes the signature, and modifies the payload (e.g., "role": "admin"). Libraries that accept none verify the token as valid with no signature check:
# Original token (simplified)
header: {"alg":"RS256"} | payload: {"sub":"1","role":"user"} | <signature>
# Forged token — no signature, role escalated
header: {"alg":"none"} | payload: {"sub":"1","role":"admin"} |
Algorithm confusion (RS256 → HS256)
When a server uses RS256, the public key is typically discoverable. If the library accepts both RS256 and HS256, an attacker signs a token with HS256 using the public key as the HMAC secret — the server then verifies the HMAC signature using that same public key.
Vulnerable code examples
Node.js — accepting any algorithm
// VULNERABLE: Allows 'none' algorithm; attacker can forge unsigned tokens
const decoded = jwt.verify(token, publicKey); // No algorithms restriction
Python — no verification
# VULNERABLE: decode() without verify=True skips signature validation entirely
import jwt
payload = jwt.decode(token, options={"verify_signature": False})
Secure code examples
Node.js — explicit algorithm allowlist
// SECURE: Explicitly restrict to expected algorithm; reject 'none'
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Allowlist — 'none' and 'HS256' are rejected
issuer: 'https://auth.example.com',
audience: 'my-api',
});
C# / ASP.NET Core — strict JWT validation
// SECURE: Validate algorithm, issuer, audience, and expiry
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(rsaPublicKey),
ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 }, // Allowlist
ValidateIssuer = true,
ValidIssuer = "https://auth.example.com",
ValidateAudience = true,
ValidAudience = "my-api",
ValidateLifetime = true,
};
});
What Offensive360 detects
nonealgorithm acceptance — JWT verification calls that do not restrict thealgorithmsparameter- Missing signature verification —
decode()calls with signature verification disabled - Weak HMAC secrets — Secrets that are short, default values, or predictable strings used with HS256/HS512
kidheader injection — Key ID values used in file paths or database queries without sanitization (can lead to path traversal or SQL injection)- Missing claims validation — Absence of
iss,aud, orexpclaim verification
Remediation guidance
-
Explicitly specify allowed algorithms — Never rely on the algorithm declared in the token header. Hard-code the expected algorithm in verification calls.
-
Never disable signature verification — Even for debugging. Use short-expiry test tokens with real signatures.
-
Use strong, randomly-generated secrets — For HS256, use at least 256 bits of cryptographically random data. For production, prefer RS256 or ES256 asymmetric signing.
-
Validate all standard claims — Always check
exp(expiry),iss(issuer), andaud(audience) in verification logic. -
Rotate secrets and key pairs — Implement key rotation with short-lived tokens to limit the blast radius of a compromised key.