Back to Blog
SecurityNovember 22, 20257 min read

JWT Security: The 5 Vulnerabilities That Still Break Production Apps in 2026

JSON Web Tokens are the dominant authentication mechanism for APIs and SPAs. They're also consistently misconfigured in ways that lead to authentication bypass. This post covers the 5 most common JWT security failures and how to test for them.

JSON Web Tokens are everywhere. They're the default authentication mechanism for REST APIs, the standard for SSO federation, and the backbone of most modern SPA authentication flows. They're also implemented incorrectly often enough that JWT exploitation is a routine finding in application security assessments.

This post covers the 5 JWT vulnerabilities that still appear in production applications in 2026, how to detect them, and how to fix them.

JWT Quick Primer

A JWT has three parts: header.payload.signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  ← header (base64)
eyJ1c2VySWQiOjQyLCJyb2xlIjoidXNlciJ9.  ← payload (base64)
HMAC_SHA256(header+payload, secret)       ← signature

The signature prevents tampering — you can't modify the payload without the signing secret. Unless you can. These five vulnerabilities demonstrate ways around it.

Vulnerability 1: Algorithm Confusion (alg:none)

The JWT spec allows an alg field in the header set to none, which means "no signature required." Some implementations honor this.

Attack:

import base64, json

header = base64.b64encode(
    json.dumps({"alg": "none", "typ": "JWT"}).encode()
).rstrip(b'=')

payload = base64.b64encode(
    json.dumps({"userId": 1, "role": "admin"}).encode()
).rstrip(b'=')

token = f"{header.decode()}.{payload.decode()}."  # empty signature

If the server accepts this token, it's verifying nothing about the signature.

Fix:

# Always specify a strict algorithm allowlist
jwt.decode(token, secret, algorithms=["HS256"])
# Never: algorithms=["HS256", "none"] or algorithms=None

How to test: Modify a valid token's header to alg:none, strip the signature, and submit. If the response is 200, the vulnerability is confirmed.

Vulnerability 2: Algorithm Switching (HS256/RS256 Confusion)

Some applications support both HMAC (symmetric, HS256) and RSA (asymmetric, RS256) algorithms. The attack: take a token meant to be verified with RS256 (using a private key), but submit it signed with HS256 using the public key as the secret.

The confusion:

If the server verifies HS256 tokens using the RS256 public key (which is often publicly accessible), an attacker can sign arbitrary payloads.

Attack scenario:

  1. Download the server's public RSA key (often at /.well-known/jwks.json)
  2. Create a JWT with alg: HS256
  3. Sign it with the public key as the HMAC secret
  4. Submit — server verifies HS256 using the public key → succeeds

Fix: Enforce the expected algorithm per client/key. If a token arrives claiming RS256, verify with RSA. If it claims HS256, verify with HMAC. Never use the same key for both.

Vulnerability 3: Weak Signing Secret

HS256 tokens signed with a weak, short, or common secret can be brute-forced offline. The entire JWT is visible to the attacker — they can crack the secret using dictionary attacks without making any requests to the server.

Attack:

# Using hashcat
hashcat -a 0 -m 16500 token.txt /usr/share/wordlists/rockyou.txt

# Using jwt-cracker
jwt-cracker eyJ... -a "abcdefghijklmnopqrstuvwxyz"

Common weak secrets seen in the wild: secret, password, jwt_secret, application name, environment variable defaults.

Fix: JWT signing secrets must be:

Vulnerability 4: Missing Expiry Validation

JWTs can include an exp (expiration) claim. If the application doesn't enforce it, tokens are valid forever — including tokens obtained by phishing, session hijacking, or from log files.

Detection:

  1. Obtain a valid JWT
  2. Wait for the exp timestamp to pass (or modify it to a past timestamp)
  3. Submit the expired token
  4. If the response is 200, expiry isn't validated

Fix:

# Enforce expiry validation explicitly
jwt.decode(
    token,
    secret,
    algorithms=["HS256"],
    options={"require": ["exp"], "verify_exp": True}  # don't disable this
)

Vulnerability 5: Insufficient Claim Validation

Even when signature and expiry are correctly validated, some applications trust JWT claims too broadly:

Missing audience (aud) check: A token issued for api.yourapp.com is also valid at admin.yourapp.com because neither checks the aud claim.

Missing issuer (iss) check: A token issued by your development instance is valid in production.

Missing subject (sub) check: A token re-used for a different user than it was issued to.

Attack example:

  1. Obtain a valid token from a partner integration (meant for their API)
  2. Submit it to your main API
  3. If aud isn't validated, it may succeed

Fix:

jwt.decode(
    token,
    secret,
    algorithms=["HS256"],
    audience="api.yourapp.com",   # must match
    issuer="auth.yourapp.com",    # must match
    options={"require": ["exp", "iat", "sub", "aud", "iss"]}
)

Testing JWT Security Systematically

A complete JWT security assessment for an API:

1. Extract a valid JWT from any authenticated request
2. Test alg:none: modify header, strip signature
3. Test algorithm confusion: if RS256, try HS256 with public key
4. Test expiry: submit expired token
5. Test claim manipulation: modify role/admin/group claims
6. Test weak secret: run offline dictionary attack
7. Test audience/issuer: submit token to other endpoints/services

Most JWT libraries implement security correctly when used as intended. The vulnerabilities appear when developers override defaults, disable validation for "debugging," or implement custom JWT parsing logic.

The rule: use battle-tested JWT libraries, never disable signature validation, and always specify an explicit algorithm allowlist. JWT security is a solved problem — the failures are implementation failures, not design failures.


PentestCheck tests for all 5 JWT vulnerabilities during DAST scans, including algorithm confusion attacks against RS256 endpoints.

Scan your attack surface today

Free plan includes 3 targets and 10 scans/month. No credit card required.

Start Free Scan