JWT attacks exploit implementation flaws in JSON Web Token validation — from the infamous 'none' algorithm bypass to algorithm confusion attacks that forge admin tokens with a public key. Here's the complete prevention guide.
Anay Pandya
1,809 words
Anay Pandya
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
JWT attacks are a category of authentication vulnerabilities that exploit weaknesses in how JSON Web Tokens are generated, signed, and validated. Because JWTs are used as authentication tokens in the vast majority of modern web APIs, a single JWT vulnerability can bypass all application-level authorization and grant attackers complete control over any account — including admin accounts. Understanding how these attacks work is essential for any developer or security professional building API-based systems.
<div class="zf-stat-callout" style="background:#0d1117;border:1px solid rgba(16,185,129,0.25);border-left:3px solid #10b981;border-radius:4px;padding:16px 20px;margin:24px 0"> <p style="margin:0 0 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.15em;color:#10b981;font-family:monospace">ZeriFlow Data — 12,400+ sites analyzed</p> <p style="margin:0;font-size:13px;color:#e2e8f0;line-height:1.6;font-family:monospace">In ZeriFlow's dataset of 12,400+ scanned sites, the average security score is 52/100 — with 68% failing at least one critical check in categories including TLS configuration, , DNS authentication, and cookie handling.</p> </div>
Keep reading
Is your site actually secure?
Run a free check — 60 seconds
A JWT consists of three base64url-encoded parts separated by periods:
header.payload.signatureHeader (algorithm and token type):
{
"alg": "HS256",
"typ": "JWT"
}Payload (claims):
{
"sub": "user123",
"role": "user",
"exp": 1735689600
}Signature: HMAC-SHA256(base64url(header) + '.' + base64url(payload), secret)
The signature cryptographically binds the header and payload to the secret. Any modification to either part invalidates the signature — *if* the validation is implemented correctly.
This is the most famous JWT vulnerability and should not exist in any modern library, yet it persists in custom implementations and some outdated libraries.
The JWT spec includes "alg": "none" as a valid algorithm meaning 'unsigned token'. If a library accepts none as a valid algorithm when validating tokens, an attacker can create a forged token with any payload and no valid signature:
Original token header:
{"alg": "HS256", "typ": "JWT"}Forged token header:
{"alg": "none", "typ": "JWT"}Forged token (signature is empty or omitted):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.The payload sets role: admin. If the server validates this token and accepts none as an algorithm, authentication is completely bypassed.
Never accept none as a valid algorithm. Explicitly whitelist the algorithms your application uses:
# PyJWT — CORRECT
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=['HS256'] # explicit whitelist
)
# WRONG — allows none bypass
payload = jwt.decode(token, SECRET_KEY)This is a more sophisticated and broadly impactful attack against systems using asymmetric algorithms (RS256, ES256).
RSA-based JWT systems use a private key to sign tokens and a public key to verify them. The public key is often publicly accessible (JWKS endpoint, documentation, even embedded in client code).
The attack exploits the fact that some libraries — when verifying an HS256 token — use the same key for both signing and verification. If a server generates RS256 tokens but doesn't validate that incoming tokens use RS256, an attacker can:
/.well-known/jwks.json)"alg": "HS256" in the headerThe server takes the public key (which it uses for RS256 verification), and uses it as the HMAC secret for HS256 verification — because that's what the token's alg header says to do. The signatures match.
Result: The attacker forges a valid token with any payload they choose — including "role": "admin".
Never trust the algorithm specified in the JWT header. The server must enforce its expected algorithm:
# SAFE — algorithm enforced server-side
payload = jwt.decode(
token,
PUBLIC_KEY,
algorithms=['RS256'] # explicitly only RS256 is accepted
)// Node.js — SAFE
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, callback);The algorithm whitelist must be configured on the *verifier*, not derived from the token header.
HS256, HS384, and HS512 tokens are signed with a symmetric secret. If that secret is weak, it can be brute-forced offline.
An attacker who obtains a JWT (from an API response, a logged request, a leak) can attempt to brute-force the signing secret offline. The token and signature are public. The only unknown is the secret.
Tools like hashcat and jwt_tool support offline JWT secret cracking:
hashcat -a 0 -m 16500 captured.jwt /usr/share/wordlists/rockyou.txtCommon weak secrets found in real applications:
- secret
- password
- jwt_secret
- Application name
- Default framework secrets left in place
- Short randomly-generated secrets (< 32 bytes)
Once the secret is found, the attacker can sign arbitrary tokens with any payload.
import secrets
JWT_SECRET = secrets.token_hex(32) # 256-bit random secretFor high-security applications, prefer asymmetric algorithms (RS256, ES256) over symmetric ones. A compromised private key can be rotated; a compromised symmetric secret invalidates all existing tokens and requires immediate rotation.
The JWT kid (Key ID) header parameter is a hint to the server about which key to use for verification. If the server uses kid in an unsafe way, it becomes an injection vector.
If the server uses kid to look up the key in a database:
# VULNERABLE
key = db.query(f"SELECT key_value FROM jwt_keys WHERE id = '{kid}'")An attacker sets kid to a SQL injection payload:
{
"alg": "HS256",
"kid": "1' UNION SELECT 'attacker_controlled_secret' --"
}The query returns 'attacker_controlled_secret' as the key. The attacker then signs the forged token with that same string, and the signature is valid.
If the server reads the key from a file system path indicated by kid:
# VULNERABLE
key_path = f'/keys/{kid}'
with open(key_path, 'r') as f:
key = f.read()Attacker sets kid to ../../dev/null (or any known file), signs the token with the content of that file (e.g., empty string for /dev/null), and forges a valid token.
kid against a strict whitelist of expected key identifierskid directly in database queries or file paths# SAFE
VALID_KEYS = {
'key-2024-01': 'secret_key_1',
'key-2024-02': 'secret_key_2',
}
def get_signing_key(kid):
key = VALID_KEYS.get(kid)
if not key:
raise InvalidTokenError('Unknown key ID')
return keySome implementations decode and use JWT payloads without validating the exp (expiration) claim. Always validate expiration:
# PyJWT validates exp by default
# To check: ensure options={'verify_exp': True} is not disabledJWTs are stateless by design — the server doesn't store issued tokens. This means a compromised token remains valid until it expires. For security-sensitive applications, implement token revocation:
['HS256'] or ['RS256'] — never both, never none)exp, iat, iss, audkid header parameter against a whitelist.env filesA: Most major JWT libraries have patched the none bypass. However, custom implementations, outdated library versions, and libraries that don't enforce algorithm whitelisting are still vulnerable. Always specify an explicit algorithm whitelist when decoding tokens, regardless of your library.
A: Obtain a valid JWT from your application. Change the alg header to HS256 (if it uses RS256). Get the application's public key (from JWKS endpoint or documentation). Sign the modified token with the public key as the HMAC secret. If the forged token is accepted, you're vulnerable. Tools like jwt_tool automate this test.
A: JWT is best suited for stateless API authentication, service-to-service authentication, and short-lived access tokens. For traditional web session management, server-side sessions with opaque session tokens (stored in HttpOnly cookies) are simpler, more secure, and easier to revoke. JWT's statelessness is a trade-off: scalability vs. revocability.
A: JWKS (JSON Web Key Set) is a standard endpoint (usually /.well-known/jwks.json) that exposes your service's public keys for asymmetric JWT verification. It's standard practice for public-facing authentication servers (OAuth providers, etc.) and enables key rotation without coordination. If your JWT signing keys are truly public (RS256 public keys), a JWKS endpoint is appropriate. Never expose symmetric secrets.
A: Yes. Log and monitor for: JWT decode failures (invalid signatures, expired tokens), unusual kid values, alg: none in token headers, tokens with impossible iat (issued in the future), and high volumes of failed authentications from the same IP. These are indicators of active JWT attack attempts.
JWT attacks span from embarrassingly simple (none algorithm bypass) to technically sophisticated (algorithm confusion, kid injection). The common thread is that most of these vulnerabilities come from trusting the token header rather than enforcing server-side policy. The fix is always the same direction: make your server explicit about what it accepts, use modern well-maintained libraries, and validate all claims rigorously.
Combined with a strong security configuration baseline — checked and maintained automatically — you create a defense-in-depth posture that makes your application significantly harder to compromise.
<!-- zf-internal-links -->
Related resources
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.
June 24, 2026
Best Security Tools for AI Startups
Monitor your security score
Schedule recurring scans and catch regressions after deployments.