JWT vulnerabilities: how attackers forge tokens and how to stop them
A JWT vulnerability lets an attacker get a forged or stolen token accepted by your API. Common causes are accepting the none algorithm, trusting the algorithm named in the token header, weak HS256 secrets, unvalidated kid, jku or jwk headers, and skipped iss, aud or exp checks. Fix them by pinning one algorithm per key and verifying every claim with a maintained library.
JWT vulnerabilities are flaws in how an application issues, stores or verifies JSON Web Tokens. A JWT is a signed bundle of claims, defined in RFC 7519, that an API reads to learn who the caller is. If the server accepts a token that the attacker made or changed, the attacker becomes any user the claims name. CWE-347, Improper Verification of Cryptographic Signature, covers the main class.
The standard anchor for fixes is RFC 8725, the JWT best current practice (BCP 225, February 2020). This page walks through eight attacks, library CVEs, safe verification code for four stacks, a JWT versus session table and a test list.
What is the one rule behind most JWT attacks?#
Decoded is not verified. A JWT has three base64url parts: header, payload and signature. Any client can decode the first two. Only a signature check with a key the server chose proves that the issuer wrote them.
Most JWT flaws come from code that reads claims before, or without, a correct signature check. Some come from code that lets the token decide how it is checked. RFC 8725 section 3.1 says libraries must let the caller state the allowed algorithms, and each key should be used with one algorithm.
Illustrative token: Harbor Notes
This is a demonstration. No real customer system was tested. Harbor Notes is a fictional app and example.com is a reserved example domain.
A test account receives this header and payload (decoded for reading):
{ "alg": "RS256", "typ": "JWT", "kid": "2026-10" }
{ "iss": "https://auth.harbor-notes.example.com", "aud": "api.harbor-notes.example.com",
"sub": "user-1042", "role": "member", "exp": 1788912000 }
The server should verify this token with the public key for kid 2026-10, algorithm RS256, issuer and audience pinned. The attacks below target each of those four choices.
How do the main JWT attacks work?#
Eight attack patterns cover most real findings. The table lists the flaw, the test and the fix. PortSwigger's JWT lessons document the same patterns with labs.
| Attack | Flaw in the server | How a tester checks it | Fix |
|---|---|---|---|
| alg none | Library accepts unsigned tokens | Resend a test token with header alg none and an empty signature | Pin allowed algorithms; reject none |
| Key confusion | Algorithm chosen from the header; one verify call for RSA and HMAC keys | Send an HS256 token signed with the RS256 public key | One algorithm per key; typed key objects |
| Weak HS256 secret | Short or guessable shared secret | Offline wordlist check of a token from your own test account | Random secret of at least 32 bytes, or asymmetric keys |
| kid injection | kid is used in a file path or SQL query | Send kid values with path or quote characters and watch for errors | Look up kid in an allow-list map |
| jku, x5u, jwk injection | Server fetches or trusts keys named in the header | Point jku at a host you control and see if the server calls it | Ignore these headers; use a fixed JWKS URL |
| Missing claim checks | iss, aud, exp or nbf not validated | Replay an expired token; present a token meant for another API | Require and validate each claim |
| Theft and replay | Token in logs, URLs or localStorage; long lifetime | Look for tokens in URLs, referrers, logs and storage | Short lifetime, HttpOnly cookie, TLS |
| Revocation gaps | Logout or password change does not end old tokens | Use a token after logout and after a password reset | jti deny list, token versions, refresh rotation |
Attacks 1 to 3: alg none, key confusion and weak secrets#
The alg header names the signing algorithm. RFC 7518 allows the value none for unsecured tokens. A server that honors the header will accept a token with no signature. PortSwigger documents this with the header {"alg":"none"}. Current libraries are built to refuse it, but old versions and custom code can still accept it, so testers try it.
Key confusion affects servers that verify RS256 tokens with a public key. If the code passes that same key to a generic verify function and the token says HS256, the library may treat the public key as an HMAC secret. Anyone knows the public key, so anyone can sign. CVE-2015-9235, published 29 May 2018, describes this in the jsonwebtoken Node module before 4.2.2: an attacker could send a token signed with an HS algorithm instead of the expected asymmetric one and bypass verification.
HS256 uses one shared secret for signing and verifying. If the secret is short or a default such as a framework sample value, an attacker can test guesses offline against any token they hold. No requests to your server are needed, so rate limits do not help. Use a random secret of at least 256 bits, or move to RS256 or ES256 with key pairs.
Attacks 4 to 6: header injection and missing claim checks#
The kid header picks the key. If the server turns kid into a file path or a database query without checks, it can be pushed to read an unintended file or run SQL. Treat kid as a lookup key into a fixed map, never as a path or a query fragment.
The jku, x5u and jwk headers can carry a key location or a key. A server that fetches or trusts them lets the token supply the key that verifies it. PortSwigger lists these as header parameter injections. A server should take keys only from a JWKS URL in its own configuration.
Claim checks are a separate control. A valid signature does not prove the token was meant for this API. Check iss (issuer), aud (audience), exp (expiry) and nbf (not before). RFC 8725 covers issuer and audience in sections 3.8 and 3.9, and typ in section 3.11. Without an aud check, a token that your identity provider issued for a different app can be replayed against yours.
Attacks 7 and 8: theft, replay and revocation#
A JWT is a bearer token. Whoever holds it can use it until it expires. Tokens leak through URLs, server logs, browser storage read by XSS, and proxies. Short lifetimes limit the window.
Revocation is the weak spot of stateless tokens. After logout or a password reset, an old access token still verifies. Plan for this: keep access tokens short (minutes), store refresh tokens on the server with rotation, and keep a deny list or a per-user token version for urgent cases.
Which JWT vulnerabilities became CVEs?#
Library bugs show the same patterns. NVD records these entries, checked 9 November 2026.
| CVE | Published | Product | What it was |
|---|---|---|---|
| CVE-2015-9235 | 29 May 2018 | jsonwebtoken (Node) before 4.2.2 | Token signed with an HS algorithm accepted where an RS or ES key was expected |
| CVE-2022-21449 | 19 April 2022 | Oracle Java SE 17.0.2 and 18, GraalVM EE 21.3.1 and 22.0.0.2 | Flaw in the Libraries component, rated CVSS 3.1 base score 7.5 (integrity); NVD notes it can be reached through APIs in a web service |
| CVE-2022-23539 | 23 December 2022 | jsonwebtoken (Node) 8.5.1 and earlier | Misconfiguration could allow legacy key types, such as DSA keys with RS256 |
NVD files CVE-2022-21449 under the Oracle Java SE Libraries component and does not name JWT. Read the Oracle advisory for your Java version before you decide whether your token code is exposed. Another ID that circulates for jsonwebtoken, CVE-2022-23529, is marked rejected in NVD, so do not cite it.
How do penetration testers detect and prove a JWT flaw?#
A finding needs a forged or altered token that the server accepts and acts on. A decoded token that shows claims is not a finding, because decoding is expected. This follows the evidence standard of our method: a write or access is proven by a read-back through another session, and controls run beside every attack request.
A test on one endpoint looks like this:
- Log in as test user A and test user B. Save both tokens.
- Decode the tokens and note alg, kid, iss, aud, exp and any role claim. Check the JWKS endpoint if one exists.
- Run controls first. A token with a flipped signature byte must get 401. An expired token must get 401. A token for another audience must get 401.
- Try one change at a time: alg none, an algorithm switch, a changed sub, a changed role. Send each to an endpoint that returns user-specific data.
- Read back. If a changed sub returns user B's record with user A's session, the signature check failed. Save the request and response with a timestamp.
Sample evidence note
Demonstration only. Harbor Notes is fictional. Control: token with one signature byte changed returns 401. Test: token with alg set to none and an empty signature returns 200 with the record of test user B. Read-back: GET /api/v1/me returns user B's email. Result: the server accepts unsigned tokens. Severity: Critical on the four-level scale.
Weak-secret checks use a token from the tester's own account and run offline. Tools such as jwt_tool and the Burp JWT Editor extension help with header edits. All of this needs written authorization, and destructive proofs use only test-owned accounts and records.
How do you verify JWTs safely in code?#
Use a maintained library, name the algorithm, and require the claims. The examples below pin RS256, the issuer and the audience. Adapt the names to your setup.
// Node.js, jsonwebtoken
const jwt = require("jsonwebtoken");
const claims = jwt.verify(token, publicKeyPem, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "api.example.com",
});
# Python, PyJWT
import jwt
claims = jwt.decode(
token, public_key_pem,
algorithms=["RS256"],
audience="api.example.com",
issuer="https://auth.example.com",
options={"require": ["exp", "iss", "aud"]},
)
// Java, jjwt 0.12
Claims claims = Jwts.parser()
.verifyWith(publicKey) // a PublicKey object, not a String
.requireIssuer("https://auth.example.com")
.requireAudience("api.example.com")
.build()
.parseSignedClaims(token)
.getPayload();
// Go, golang-jwt v5
tok, err := jwt.Parse(raw, func(t *jwt.Token) (any, error) {
return publicKey, nil
}, jwt.WithValidMethods([]string{"RS256"}),
jwt.WithIssuer("https://auth.example.com"),
jwt.WithAudience("api.example.com"),
jwt.WithExpirationRequired())
Pass the algorithm list to the verify call itself, not to a later check. Never fall back to a default when the list is missing.
Also set a lifetime of minutes for access tokens, load keys from a fixed JWKS URL you control, rotate keys with new kid values, and keep secrets out of the payload.
Should you use a JWT or a server-side session?#
Use a JWT when several services must verify identity without calling a central store. Use a session when one application owns the user and you need instant logout.
| Question | JWT access token | Server-side session |
|---|---|---|
| Revocation | Hard: wait for expiry or add state | Delete the session record |
| Verification | Any service with the public key | Lookup in a shared store |
| Size | Grows with claims | Small cookie value |
| Storage in browser | HttpOnly cookie is safer than localStorage | HttpOnly, Secure, SameSite cookie |
| Failure mode | Forged token if verification is wrong | Session store outage |
A JWT in an HttpOnly cookie needs CSRF defenses. A JWT in localStorage needs XSS defenses, and one XSS bug exposes all tokens. See the authentication testing checklist for session tests.
JWT testing checklist#
- Decode every token type (access, refresh, ID) and list alg, kid, iss, aud, exp and claims that drive authorization.
- Confirm the server rejects: tampered signature, alg none, a switched algorithm, expired, not yet valid, wrong issuer, wrong audience.
- Check HS256 secrets for strength against a wordlist, using tokens from test accounts only.
- Test kid, jku, x5u and jwk handling with harmless values and watch outbound requests from the server.
- Confirm authorization uses server-side data, not a role claim a client could have influenced at login.
- Check lifetime, refresh rotation, and behavior after logout and password reset.
- Search logs, URLs, referrers and browser storage for tokens.
- Compare responses for anonymous, user A and user B callers on each endpoint.
When should you get a professional JWT test?#
Get one when tokens carry roles or tenant IDs, when several services verify them, or before a launch or customer security review. A tester checks signing, claims, storage and revocation across your real endpoints, with two or more roles. Our API penetration testing and web application penetration testing cover token handling as part of scope. Testing starts only after written authorization from the system owner. Token flaws often sit next to OAuth vulnerabilities, because OAuth login often ends in a JWT.
Questions
Is a JWT encrypted?
Usually not. A signed JWT (JWS) is encoded with base64url, so anyone who holds it can read the claims. The signature only proves who issued it and that it was not changed. Do not put secrets in a signed JWT. RFC 7516 defines encrypted tokens (JWE) for that need.
How do I check whether my JWT library accepts alg none?
On a test environment, send a token from a test account with its header changed to alg none and the signature removed, to an endpoint that needs authentication. A secure library rejects it. Also check that your verify call names an algorithm list, because a default is not a safe control.
Should I store a JWT in localStorage or a cookie?
Any script that runs on your page can read localStorage, so one XSS flaw leaks every token. An HttpOnly, Secure, SameSite cookie hides the token from scripts but needs CSRF protection. Neither is free. Pick the one whose failure you can defend against.
How do I revoke a JWT?
A signed JWT stays valid until it expires, so revocation needs state. Use short access token lifetimes, a deny list keyed by the jti claim, or a per-user token version that you compare on each request. Refresh tokens need server-side storage and rotation.
What is JWT key confusion?
It is an attack on servers that choose the verification algorithm from the token header. The attacker switches an RS256 token to HS256 and signs it with the public key as the HMAC secret. Pinning the algorithm in the verify call stops it.
Sources
- RFC 8725: JSON Web Token Best Current Practices (BCP 225) (checked 9 November 2026)
- RFC 9700: Best Current Practice for OAuth 2.0 Security (BCP 240) (checked 9 November 2026)
- PortSwigger Web Security Academy: JWT attacks (checked 9 November 2026)
- NVD: CVE-2015-9235 (jsonwebtoken before 4.2.2) (checked 9 November 2026)
- NVD: CVE-2022-21449 (Oracle Java SE ECDSA signature validation) (checked 9 November 2026)
- CWE-347: Improper Verification of Cryptographic Signature (checked 9 November 2026)