Web Security

JWT Security: Attacks on JSON Web Tokens and How to Prevent Them

A complete technical guide to JWT security: alg:none attacks, weak secret cracking, kid injection, JWK header injection, token confusion, and proper validation for Node.js, Python, and Go.

Intermediate14 min·Nyxeara Security Research·2026-09-21·CWE-345
jwtjson-web-tokenauthenticationtoken-securityowasp-top-10web-securityapi-security
Prerequisites
what-is-httpauthentication-vulnerabilities

Short answer

JWT vulnerabilities exist because the token format separates the algorithm declaration from the validation logic — an attacker can change the algorithm, and if the server doesn't verify which algorithm was actually used to sign the token, it accepts the modified token as authentic.

The idea in one minute

A JWT is a card that says "I am Alice" and has a signature to prove it. The signature is made with a secret stamp kept by the server. But the card also says on the front "I was stamped using Algorithm X." If the server reads the algorithm from the card instead of enforcing it at the door, an attacker can scratch out "Algorithm X (secure)" and write "Algorithm None (no stamp required)" — and walk right in.

This is not a theoretical bug. It has been exploitable in nearly every JWT library in every language at some point. The vulnerability is not in the JWT format itself — it is in the assumption that the token tells the server how to verify its own integrity.

How JWT actually works

A JSON Web Token is three Base64-URL-encoded segments separated by dots:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWxpY2UifQ.fake_signature

The first segment is the header: {"alg":"HS256"}. The second is the payload: {"user":"alice"}. The third is the signature (when present). The header declares which algorithm was used and, for asymmetric algorithms, may include the key URL (jku) or the key itself (jwk).

The server decodes the header, reads the algorithm, and attempts to verify using the key it expects for that algorithm. The problem: the algorithm comes from the token, not from a configuration file. If the token says alg: none and the library doesn't explicitly reject it, the signature check is skipped entirely.

Common JWT attacks

1. Algorithm none attack

The attacker changes the header from {"alg":"HS256"} to {"alg":"none"} and removes the signature entirely:

import base64, json
header = base64.urlsafe_b64encode(json.dumps({"alg":"none"}).encode()).rstrip(b"=").decode()
payload = base64.urlsafe_b64encode(json.dumps({"user":"admin","role":"admin"}).encode()).rstrip(b"=").decode()
token = f"{header}.{payload}."

Prevention: Explicitly reject alg: none at the application level. Set { algorithms: ['HS256'] } in the jwt.verify call. Never rely on the library's default behavior.

2. Weak secret brute force

When the server uses a symmetric algorithm (HS256, HS384, HS512), the same secret both signs and verifies tokens. If the secret is weak — a single word, a date, a common password — it can be cracked offline.

# crack JWT with john
python jwt_tool.py eyJhbG... -C -d rockyou.txt

Common weak JWT secrets found in real audits: secret, secret123, jwt_secret, supersecret, changeme, the application's name, the company name, password, 123456.

Prevention: Use a random 256-bit (32-byte) secret generated by openssl rand -hex 32. Rotate secrets periodically. Never share symmetric secrets across environments.

3. JWK header injection

The server may accept a jwk (JSON Web Key) in the token header and use the attacker-supplied public key to verify the signature:

{
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "n": "attacker-generated-modulus",
    "e": "AQAB"
  }
}

Prevention: Pin the allowed public keys server-side. Never accept a key from the token itself. Disable the jwk and jku header parameters unless you are certain of your library's validation of them.

4. KID injection (path traversal)

The kid (Key ID) header parameter is used to look up the verification key from a database or filesystem. If the server interpolates the KID value into a file path, it becomes path traversal:

{
  "alg": "HS256",
  "kid": "../../etc/passwd"
}

Prevention: Validate KID against an allowlist. Never use KID values to construct file system paths. If KID maps to database records, parameterize the query.

5. Algorithm confusion (RS256 vs HS256)

The server expects RS256 (asymmetric RSA) but the attacker sends a token with HS256 (symmetric HMAC) signed with the public key. Since the public key is often obtainable — from the application's source, a .well-known endpoint, or the JWKS endpoint — the attacker uses the public key as the HMAC secret:

import hmac, hashlib, base64, json
public_key = open("public.pem").read()
header = base64.urlsafe_b64encode(json.dumps({"alg":"HS256"}).encode())
payload = base64.urlsafe_b64encode(json.dumps({"user":"admin"}).encode())
signature = hmac.new(public_key.encode(), f"{header}.{payload}".encode(), hashlib.sha256).digest()
token = f"{header}.{payload}.{base64.urlsafe_b64encode(signature)}"

Prevention: Enforce a strict algorithm allowlist on the verification call. Never pass user-controlled algorithm values to the verify function.

Detection

Send a JWT with alg: none and an empty signature to every endpoint that accepts a Bearer token. A 401 with "invalid signature" is expected; a 200 is critical. If alg: none is rejected, try alg: None, alg: NONE, alg: nOnE. Some libraries only check lowercase. Then try algorithm confusion: obtain the server's public key (from JWKS endpoint or source code), create an HS256 token signed with the public key, and send it. If accepted, the server is vulnerable.

Verification: real vulnerability or false positive?

alg: none returning a 200 with authenticated data is an unconditional vulnerability — it means the server does not verify signatures at all. Testing on authorized targets only. For JWK injection: send a token with a self-generated RSA key pair in the jwk header. If the server accepts signatures from your key, it is vulnerable.

Real-world impact

GitHub Enterprise Server had a JWT algorithm confusion vulnerability (CVE-2019-13601) that allowed remote attackers to forge administrative sessions with a single HTTP request. Auth0's own Node.js library had an alg: none bypass. The Ruby JWT library accepted alg: none by default until version 2.0 — a default that left thousands of applications vulnerable. JWT vulnerabilities are not edge cases; they are the default behavior of most libraries at some point in their history.

Prevention checklist

  1. Explicitly set { algorithms: ['HS256'] } in the verify call — never accept the algorithm from the token header.
  2. Reject alg: none, alg: None, and every case variant explicitly.
  3. Use asymmetric keys (RS256, ES256) instead of symmetric when possible.
  4. Never accept jwk or jku headers from tokens. Pin key IDs server-side.
  5. Validate KID values against an allowlist — never use them for file system access.
  6. Use a 256-bit random secret: openssl rand -hex 32.
  7. Implement short token expiration (15 minutes for access tokens, 7 days for refresh tokens).
  8. Rotate signing keys at least quarterly and immediately after any suspected compromise.

Related vulnerabilities

  • Authentication vulnerabilities — JWT weaknesses are a subclass of broken authentication.
  • CORS misconfiguration — Permissive CORS with credentialed requests enables token theft via XSS.
  • Information disclosure — JWT payloads are base64-encoded, not encrypted; sensitive data in the payload is trivially readable.

Testing methodology (do this safely)

Obtain a valid JWT from the application. Decode the header and payload using base64 -d or jwt.io. Try each attack: alg: none, algorithm confusion, weak secret cracking, KID path traversal. Test on your own applications or authorized bug bounty programs. Be aware that cracking weak secrets is detectable and should only be done with explicit authorization.

Further reading

Nyxeara perspective

The Nyxeara verification engine has a dedicated JWT playbook. When a CANDIDATE finding with "JWT" in the title enters the verification pipeline, the playbook sends three probes to the same target: one with alg: none, one with an empty signature, and one algorithm-confusion probe. The differential between the baseline (no token) and each test response determines whether the server actually validates signatures or just decodes the header. This deterministic test prevents the LLM from reporting a JWT vulnerability that does not exist — the verification engine only promotes a finding to VALIDATED when the server demonstrates it accepts a forged token.

Published 2026-09-21 · Updated 2026-09-21