JWT exploitation: attacks, real CVEs, and secure fixes

JWT exploitation: attacks, real CVEs, and secure fixes

Before JSON Web Tokens became widely adopted, most web applications relied on server-side sessions. Sessions work - but they introduce scalability headaches in distributed systems. JWTs solved this by making authentication stateless: all necessary information is embedded in the token itself, signed by the server. That sounds great. Until it's implemented poorly. Misconfigured JWT handling is one of the most common and dangerous security flaws in modern web apps. This post covers how JWTs work, real attack techniques with known CVEs, and concrete fixes across major frameworks.

What is a JWT?

A JWT consists of three base64url-encoded segments separated by dots.

HEADER.PAYLOAD.SIGNATURE

The header declares the token type and signing algorithm. The payload holds claims — user ID, role, expiry. The signature is computed over both using a secret or private key, and is the only thing preventing tampering.

// Header
{ "alg": "HS256", "typ": "JWT" }

// Payload
{ "user_id": 123, "role": "user", "exp": 1712000000 }

// Signature
HMACSHA256(base64url(header) + "." + base64url(payload), secret)

The critical point: if the server doesn't rigorously validate the signature or lets the client influence how it's verified everything falls apart.

Attack techniques

1. Algorithm = "none" bypass

The JWT spec allows alg: none for unsecured tokens. Vulnerable servers accept these without any signature verification, enabling a full authentication bypass.

How it works:

  1. 1. Decode the token (base64url decode each segment)

  2. 2. Modify the payload — e.g. set "role": "admin"

  3. 3. Change the header to "alg": "none"

  4. 4. Re-encode and send with an empty signature: HEADER.PAYLOAD.

Fix — whitelist allowed algorithms server-side and never accept "none":

// Node.js
jwt.verify(token, secret, { algorithms: ["HS256"] });

// Python
jwt.decode(token, key, algorithms=["HS256"])

Real CVE: CVE-2015-9235 — Auth0's jsonwebtoken library accepted alg: none tokens, allowing authentication bypass without a valid signature.

2. RS256 → HS256 algorithm confusion

When a server uses RS256 (asymmetric), the public key is often published at /.well-known/jwks.json. If the server trusts the client-supplied alg field and falls back to HS256, an attacker can sign a forged token using the public key as the HMAC secret — and the server will verify it successfully.

How it works:

Fix — never trust the client-supplied algorithm. Enforce it server-side:

// Java / Spring Boot
Jwts.parserBuilder()
  .requireAlgorithm(SignatureAlgorithm.RS256)
  .setSigningKey(publicKey)
  .build();

Real CVE: CVE-2016-10555 — jwt-simple for Node.js did not validate the algorithm field, enabling RS256 → HS256 confusion attacks.

3. JWK header spoofing

The JWT spec supports an optional jwk header parameter that embeds the signing key directly in the token. Servers that trust this field allow an attacker to supply their own key and sign tokens with it.

{
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "n": "<attacker public key>",
    "e": "AQAB"
  }
}

Fix: Ignore jwk, jku, and x5u headers from tokens entirely. Only load keys from your own trusted key store never from the token itself.

4. kid (key ID) injection

If the server uses the kid header to dynamically load a signing key without sanitization, an attacker can exploit path traversal or SQL injection to control which key is used.

Path traversal example:

{ "kid": "../../../dev/null" }
// Server does: fs.readFileSync("/keys/" + kid)
// Loads empty file → HMAC secret becomes an empty string

SQL injection example:

{ "kid": "' OR 1=1 --" }
// Unsanitized DB query → attacker controls which key is returned

Fix — use an allowlist of valid key IDs:

// Node.js
const allowedKids = ["key-2024-01", "key-2024-02"];
if (!allowedKids.includes(kid)) throw new Error("Invalid kid");

5. Weak secret brute-force (HS256)

HS256 tokens signed with weak secrets like secret, password, or 123456 can be cracked offline. An attacker only needs a valid token and a wordlist.

john --wordlist=rockyou.txt jwt.txt
# or
hashcat -a 0 -m 16500 token.txt wordlist.txt

Fix — use a cryptographically random secret of at least 256 bits:

openssl rand -base64 64

Store it in an environment variable. Never hardcode it in source files or commit it to version control.

Real CVE: CVE-2022-21449 ("Psychic Signatures") — Java's ECDSA verification accepted blank signatures in JDK 15–18, bypassing JWT validation entirely. A reminder that even standard library implementations can fail silently.

Secure implementation by framework

Below are minimal secure examples for each major framework. All of them share the same principles: enforce the algorithm, validate issuer and audience, and load secrets from environment variables only.

// Node.js / Express
jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ["HS256"],
  issuer: "your-app",
  audience: "your-client"
});

# Django (SimpleJWT)
SIMPLE_JWT = {
    "ALGORITHM": "HS256",
    "SIGNING_KEY": os.environ["JWT_SECRET"],
}

# Laravel — .env
JWT_SECRET=long_random_secure_key_here

# Usage
JWTAuth::parseToken()->authenticate();

// ASP.NET Core
services.AddAuthentication()
  .AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters {
      ValidateIssuerSigningKey = true,
      ValidateIssuer = true,
      ValidateAudience = true,
      ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256 }
    };
  });

// Spring Boot (Java)
Jwts.parserBuilder()
  .setSigningKey(secretKey)
  .requireIssuer("your-app")
  .build()
  .parseClaimsJws(token);

Key takeaways

Always do:

Never do:

JWT vulnerabilities are almost never about cryptography failing. They're about developers trusting input they shouldn't. The spec is flexible by design that flexibility is also the attack surface.

One more thing worth noting: a misconfigured JWT system is often harder to fix than sessions, because tokens can't be revoked without additional infrastructure. A token you signed yesterday is still valid tomorrow unless you've built a denylist. Keep that in mind when deciding whether JWTs are actually the right tool for your use case.

Do you like the post?

You can mail me at [email protected]

I post blogs—feel free to send feedback, suggestions, or just connect.


print('read_more_blogs')
print('back_to_portfolio')