JSON Web Tokens (JWTs) are the dominant stateless authentication mechanism for modern web applications and APIs. They appear in OAuth 2.0 and OpenID Connect flows, API gateway authentication, microservice-to-service communication, and single-page application session management. Their ubiquity makes JWT implementation vulnerabilities high-value targets — a successful JWT attack can yield complete authentication bypass, privilege escalation to administrative accounts, or full account takeover for any user in the system.
This post covers the JWT security threat landscape from a practitioner perspective: the structure, the vulnerabilities with real exploitation context, testing methodology, and what secure implementation actually looks like.
JWT Structure: What You Are Working With
A JWT consists of three base64url-encoded parts separated by periods: header.payload.signature.
The header specifies the token type and signing algorithm: {"alg": "RS256", "typ": "JWT"}. The payload contains claims — assertions about the subject: {"sub": "user123", "role": "user", "iat": 1741737600, "exp": 1741741200}. The signature is a cryptographic value computed over the header and payload using the specified algorithm and a secret or private key.
The critical security property: the signature prevents tampering with the payload. If an attacker modifies "role": "user" to "role": "admin", the signature will not validate and the server should reject the token. The vulnerabilities below all describe ways this guarantee fails in practice.
Critical JWT Vulnerabilities
1. Algorithm Confusion Attack (CVE-2015-9235 class)
This is the most impactful JWT vulnerability class. The attack exploits libraries that accept the algorithm specified in the token header rather than enforcing a fixed algorithm.
Consider an application that signs tokens using RS256 (RSA with SHA-256), where the private key signs tokens and the public key verifies them. An attacker obtains the public key (often discoverable at /.well-known/jwks.json or in application source code). The attacker crafts a malicious token with the header changed to {"alg": "HS256"} and signs it using the HMAC-SHA256 algorithm with the RS256 public key as the HMAC secret. A vulnerable server that accepts the algorithm from the header will attempt to verify the signature using HS256 with its configured key — which happens to be the same public key. Verification succeeds. The attacker has forged an arbitrary payload.
The fix: always validate the algorithm against a server-side allowlist before signature verification. Never trust the alg header value.
2. The "alg: none" Attack
Some JWT libraries support an alg: none value indicating an unsigned token. An attacker modifies a valid token's header to {"alg": "none", "typ": "JWT"}, modifies the payload arbitrarily, and removes the signature (sending header.payload. with an empty signature segment). Vulnerable libraries accept this as a valid unsigned token.
CVE-2015-9235 (node-jsonwebtoken), CVE-2022-21449 (Java ECDSA implementation, the 'Psychic Signatures' vulnerability), and numerous library CVEs in PHP and Python JWT libraries have involved algorithm validation failures. The fix: explicitly reject alg: none and other non-approved algorithms.
3. Weak Secret Brute Force
Applications using HS256 with a weak or guessable secret (short keys, dictionary words, default secrets like secret, password, or application names) are vulnerable to offline brute force. An attacker captures a valid JWT and runs it through hashcat with mode 16500 (JWT-HS256) or the dedicated jwt_tool (python3 jwt_tool.py <token> --crack -d /path/to/wordlist). On a single modern flagship GPU, hashcat tests roughly four billion HS256 signature guesses per second (RTX 4090: ~4.2 billion/s; RTX 5090: ~4.9 billion/s — Chick3nman hashcat v6.2.6 benchmarks, mode 16500). A weak or short JWT secret falls in seconds to minutes, and even a full 8-character alphanumeric secret is exhausted in well under a day (minutes on a multi-GPU rig) — which is why HS256 signing keys must be long, high-entropy random values, not human-chosen passwords.
The fix: use RS256 or ES256 for production systems. If HS256 is required, use a cryptographically random secret of at least 256 bits (32 bytes) generated by a CSPRNG.
4. kid (Key ID) Injection
The JWT header may include a kid (Key ID) parameter that the server uses to look up the correct verification key from a key store. If the application uses the kid value directly in a SQL query or filesystem path without sanitization, it is vulnerable to injection.
SQL injection via kid: an attacker sets "kid": "' UNION SELECT 'attackersecret' -- ". If the key lookup is SELECT key FROM keys WHERE id = '$kid', the injected query returns the attacker-controlled string as the key. The attacker then signs a malicious token with that same string and sends it to the server.
Path traversal via kid: "kid": "../../../dev/null" returns an empty key — combined with an empty HS256 signature computed over an empty secret, this achieves authentication bypass on vulnerable implementations.
5. JWK URL Injection
Some JWT implementations support a jku (JWK Set URL) or x5u header parameter pointing to a URL from which the server fetches verification keys. An attacker who can control this header parameter can point it to an attacker-controlled server hosting a JWK Set, then sign tokens with the corresponding private key. The server fetches keys from the attacker's URL and verifies the signature successfully.
The fix: never allow the jku or x5u header to override the configured key source. Pin key sources server-side.
Testing Methodology
JWT security assessment follows a structured workflow:
- Capture: Obtain a valid JWT from authentication flow. Inspect with
jwt.ioorpython3 -c "import base64, json; print(json.dumps(json.loads(base64.b64decode(token.split('.')[1] + '==')), indent=2))". - Algorithm tests: Use
jwt_tool(python3 jwt_tool.py <token> -X a) to test algorithm confusion and-X nfor alg:none. - Secret brute force:
python3 jwt_tool.py <token> --crack -d rockyou.txtorhashcat -a 0 -m 16500 <token> /path/to/wordlist. - Header injection: Manually craft tokens with modified
kid,jku, andx5uvalues. Test SQL injection payloads in kid. - Claim manipulation: Test role escalation (
user→admin), sub substitution (change to another user's ID), exp extension.
Secure JWT Implementation
Practical guidance for production systems:
- Use asymmetric algorithms: RS256 (RSA-PKCS1v1.5, minimum 2048-bit key) or ES256 (ECDSA with P-256, preferred for performance) for all production systems. HS256 is acceptable only for service-to-service communication where both parties equally trust the shared secret.
- Short expiration: Access tokens should expire in 15–60 minutes. Long-lived tokens increase the exposure window if compromised.
- Validate all claims: Always validate
iss(issuer),aud(audience),exp(expiration), andnbf(not before). Library defaults often omit audience validation. - Key rotation: Rotate signing keys regularly (quarterly minimum) and publish keys via a JWKS endpoint with
kidvalues. Old tokens signed with rotated keys should be rejected after expiry. - Stateful revocation: Pure stateless JWTs cannot be revoked before expiry. Maintain a token blocklist (Redis with TTL matching token expiry) for critical operations like logout, password reset, and privilege changes.
- Refresh token security: Refresh tokens should be opaque (not JWTs), stored in HttpOnly cookies (not localStorage), and subject to rotation on each use (refresh token rotation) to limit compromise blast radius.
JWT vulnerabilities are a recurring weakness uncovered in web application security assessments. If your application uses JWTs for authentication, a dedicated API and authentication security review is warranted. Contact us to discuss scope and timelines.