JSON Web Tokens are frequently misconfigured, enabling attacks from algorithm confusion to secret brute-force.
TL;DR
alg:none, RS256→HS256 downgrades, and kid injection bypass signature verification entirelyJSON Web Tokens (JWTs) are stateless bearer tokens: a signed header.payload.signature string. The client sends them in Authorization: Bearer <token> headers, and the server validates the signature to ensure integrity. However, misconfigured JWT handling is among the most common authentication flaws — surfacing in OWASP A07:2021 (Identification and Authentication Failures) for design errors and A02:2021 (Cryptographic Failures) for signature-bypass classes.
The core risk: if signature validation is bypassed or weakened, attackers can forge tokens as any user — granting unauthorized access, privilege escalation, or lateral movement across services. Because JWTs are typically self-contained (no server-side session state to invalidate), a single forged token may remain valid for the full lifetime of the original — sometimes hours or days.
JWT vulnerabilities arise from three structural properties of the format that, individually, are by-design but collectively produce repeated misconfiguration:
alg field lives in the attacker-controlled header. Every library must read the algorithm declaration before validating any signature, because the algorithm tells the library how to validate. If the library trusts this field without an explicit allowlist, the attacker controls the verification path."secret" are textually indistinguishable inside a Docker .env file. There is no compile-time, lint-time, or runtime check that flags JWT_SECRET=changeme — but hashcat -m 16500 recovers it in milliseconds.aud, another does not.The result is that JWT exploitation is rarely about breaking cryptography — it's about finding one library, one service, or one code path where the controls were misconfigured or skipped entirely.
| Class | Variant | Primary CVE | CVSS | Difficulty |
|---|---|---|---|---|
| Signature stripping | alg:none | CVE-2015-9235, CVE-2026-34950 | 9.8 | Trivial |
| Key confusion | RS256 → HS256 | CVE-2016-10555, CVE-2022-29217 | 9.1 | Easy |
| Brute-force | Weak HMAC secret | n/a (config issue) | 8.8 | Easy with GPU |
| Header injection | kid parameter | CVE-2018-0114 | 8.1 | Medium |
| Header injection | jwk / x5c / x5u | CVE-2018-0114 variants | 7.5 | Medium |
Each variant has a dedicated subpage in this section. The shared root cause across all five is trusting attacker-controlled token metadata at verification time.
Three probes confirm or rule out the most common misconfigurations:
# 1. alg:none — strip signature
echo '{"alg":"none","typ":"JWT"}' | base64url
echo '{"sub":"admin","role":"admin"}' | base64url
# Concatenate: <header>.<payload>. (trailing dot, empty sig)
curl -H "Authorization: Bearer <forged>" https://target/api/me
# 2. Weak HMAC secret
hashcat -m 16500 captured_token.txt /usr/share/wordlists/rockyou.txt
# 3. RS256 → HS256 confusion
# Sign payload with the server's RSA public key as HMAC secret
python3 -c "import jwt; print(jwt.encode({'role':'admin'}, open('server.pub').read(), algorithm='HS256'))"A 200 response on probe 1 or 3, or a recovered secret on probe 2, indicates an exploitable misconfiguration. Production endpoints should return 401 on all three.
# SAFE — explicit algorithm allowlist, audience binding, expiry enforcement
import jwt
from jwt import InvalidTokenError
try:
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"], # explicit allowlist — never trust header.alg
audience="api.example.com", # bind tokens to this API
issuer="https://auth.example.com",
options={"require": ["exp", "iat", "aud", "iss"]},
)
except InvalidTokenError:
abort(401)Additional controls:
kid against an internal allowlist — never fetch keys from URLs in the headerjwt.decode(token, verify=False) — disables signature verification entirely. Common in debugging code that ships to production.algorithms=jwt.algorithms.get_default_algorithms().keys() — accepts every algorithm the library supports, including none.kid to fetch keys from a URL or filesystem path — enables SSRF, file inclusion, and RCE depending on the lookup mechanism.exp validation because "the token will be fresh anyway" — until a token leaks via logs or a browser-extension breach.This section covers the full taxonomy of JWT misconfiguration:
A JWT is a compact, URL-safe, stateless bearer token composed of three base64url-encoded segments separated by dots: header.payload.signature. The header declares the signing algorithm (alg) and key identifier (kid). The payload contains claims such as sub (subject), iss (issuer), aud (audience), exp (expiry), and custom application data like role or tenant_id. The signature binds the header and payload using the chosen algorithm and a secret or private key. Servers validate by recomputing the signature and comparing it to the value embedded in the token. JWTs are widely used for session tokens, OAuth 2.0 access tokens, and OIDC id_tokens (RFC 7519, RFC 7515).
JWTs concentrate three classes of risk into one bearer string. First, signature trust is parser-driven: many libraries read the alg field from the attacker-controlled header and dispatch verification accordingly — enabling alg:none, RS256→HS256 confusion, and kid injection. Second, secret strength is invisible: HS256 tokens with developer-chosen secrets (under 32 bytes, dictionary words, or environment defaults like 'secret' or 'jwt-secret') fall to hashcat mode 16500 in seconds. Third, validation is decentralized: every service that consumes a JWT must independently enforce signature, expiry, and audience checks — leading to per-service drift where one microservice accepts unsigned tokens while another rejects them.
(1) alg:none — strip the signature by declaring no algorithm (CVE-2015-9235, CVE-2026-34950). (2) Weak HMAC secret — offline brute-force HS256 tokens against rockyou.txt or custom wordlists. (3) Algorithm confusion (RS256→HS256) — submit a token signed with the server's public key as the HMAC secret. (4) kid header injection — point the key identifier to a file path, database row, or attacker-controlled URL. (5) jwk/x5c/x5u header injection — embed the attacker's own public key or certificate inside the header for the server to trust on first sight. BreachVex tests all five variants and reports CVE mappings where applicable.
Run three tests against your authentication endpoint. (1) Decode a valid token, change header.alg to 'none', strip the signature segment (keep the trailing dot), resubmit — a 200 response indicates alg:none acceptance. (2) Change alg from RS256 to HS256, sign the unchanged payload with your server's RSA public key as the HMAC secret — a 200 response indicates algorithm confusion. (3) Capture an HS256 token and run hashcat -m 16500 token.txt rockyou.txt — if a secret cracks within an hour, the secret is too weak. All three should fail in a properly configured server.
OWASP A02:2021 (Cryptographic Failures) covers signature-stripping and algorithm confusion — failures of the cryptographic control itself (alg:none, RS256→HS256, weak HMAC secrets). OWASP A07:2021 (Identification and Authentication Failures) covers higher-level authentication design flaws such as missing expiry validation, missing audience binding, replay attacks, and absent revocation mechanisms. A single misconfigured JWT system often violates both categories. This overview page uses A07:2021 because it spans the broader JWT authentication topic; specific variants like alg:none map more precisely to A02:2021.
Different threat models. Server-side session cookies require database lookups but enable immediate revocation. Stateless JWTs scale horizontally and eliminate per-request database hits, but cannot be revoked before expiry without an additional revocation list — which reintroduces the state that JWTs were meant to eliminate. The pragmatic answer: short-lived access tokens (5-15 minutes) combined with a refresh-token rotation pattern give most of the scalability of JWTs while keeping the revocation window short. Long-lived JWTs (hours or days) are a security anti-pattern.
Five controls. (1) Explicit algorithm allowlist at verification: jwt.decode(token, key, algorithms=['RS256']) — never accept the alg field from the header. (2) Asymmetric keys (RS256, ES256, EdDSA) for distributed systems so public keys can be shared without compromising signing capability. (3) Strong HMAC secrets (256-bit random, never derived from passwords) if HS256 is required. (4) Mandatory iss, aud, and exp validation, plus nbf (not-before) for tokens that include it. (5) A key rotation strategy that uses kid lookups against an internal allowlist — never an attacker-controlled URL.