By AndyPublished
JWT Attacks and Vulnerabilities: How Each Works, How to Test, and the Fix
JWT Attack Types and Attack Vectors
A JWT reaches your server as three Base64url segments: header, payload and signature (see header, payload and signature explained). Every field is attacker-controlled until the signature has been checked with a key and algorithm the server chose. The attack vectors fall into four families:
- ·Algorithm handling: the verifier lets the token choose the algorithm (none, algorithm confusion).
- ·Key handling: the verifier lets the token choose or supply the key (kid, jku, x5u, jwk), or the key is guessable (weak secrets).
- ·Validation gaps: the signature or the claims are never actually checked (decode instead of verify, missing exp, aud, iss).
- ·Token handling: a legitimate token is stolen and replayed (URLs, logs, XSS against browser storage).
JWT Vulnerabilities in OWASP and RFC 8725
OWASP has no separate "JWT Top 10"; JWT flaws sit under Broken Authentication and Cryptographic Failures in the OWASP Top 10, and are covered in detail by the OWASP JSON Web Token cheat sheet and the Web Security Testing Guide. The IETF's own answer is RFC 8725, JSON Web Token Best Current Practices, written precisely because these attacks kept recurring. Its core rules: verify the algorithm against an allow-list (§3.1), use keys with enough entropy, validate issuer and audience, and use explicit typing so one kind of JWT cannot be substituted for another. The PortSwigger Web Security Academy JWT topic has free hands-on labs for most of these, which is the safest place to watch one work against a deliberately vulnerable target.
The alg none Attack
How it works. RFC 7518 defines "alg": "none" for unsecured JWTs, which carry no signature. A verifier that decides how to check a token by reading the header's alg, and that still supports none, will accept an unsigned token as valid, so any payload is trusted. Case variants such as None and NONE have historically bypassed naive string checks.
Test. Confirm your verifier returns 401 for a token whose header declares none and whose signature segment is empty. It should never depend on the header to decide whether a signature is required.
Fix. Pass an explicit algorithm allow-list to your library and never include none. The full history and per-library settings are in the none algorithm vulnerability guide.
Algorithm Confusion (RS256 to HS256)
How it works. With RS256 the server verifies using an RSA public key, which is public by design and often published at a JWKS endpoint. With HS256 the server verifies using a shared secret. The vulnerability appears when a verifier picks the algorithm from the token header and reuses the same configured key material for both families: it can be induced to treat the RSA public key as an HMAC secret. Because that "secret" is public, an attacker who knows it could produce a token the server accepts. The root cause is a verifier that both trusts the header's alg and does not bind a key to a single algorithm. HS256 and RS256 are compared in HS256 vs RS256.
Test. Confirm a service configured for RS256 rejects a token whose header says HS256, rather than trying to verify it as HMAC. A service that "supports both" without pinning is the risky shape.
Fix. Pin exactly one algorithm per key and pass an allow-list containing only the asymmetric algorithm you use. Modern libraries help by taking typed key objects (an RSA public key object cannot be used as an HMAC secret) rather than raw strings; prefer those APIs.
Weak HMAC Secrets (HS256)
How it works. HS256 security rests entirely on the secret. If the secret is a dictionary word, a short string, or a well-known example value copied from a tutorial, its signatures can be reproduced by anyone who guesses it. Because verification is offline, an attacker with a single captured token can test candidate secrets locally, at high speed, with no requests to your server, so rate limiting offers no protection. Security researchers use tools such as hashcat (JWT is hash mode 16500) and John the Ripper to demonstrate how quickly weak secrets fall; the defensive takeaway is that a weak secret is effectively no secret.
Test. Audit how the secret was generated and how long it is. Anything memorable, derived from a name, or shorter than the hash output is a finding. Treat any tutorial or default secret in production as already compromised.
Fix. Use a secret of at least 256 bits from a cryptographically secure RNG (for example openssl rand -base64 32), store it in a secret manager, and rotate it if it may have been exposed. For services where the verifier should not also be able to mint tokens, move to an asymmetric algorithm so verifiers hold only a public key.
kid Header Injection
How it works. The kid (key ID) header names which key signed the token. It is only meant to be a lookup key, but some servers feed its value into a file path, a database query or a command. An attacker who controls kid can then try to point the verifier at a key they influence: a predictable file on disk, or, via an injection, a value they supply, so the token verifies against something they control. Related header abuse (cty, x5c) shares the pattern of trusting a header field. See the kid header guide.
Test. Confirm that unusual kid values, path fragments, SQL metacharacters or an unknown ID, cannot change which key is used or reach a filesystem or database unsanitised. The verifier should simply fail closed on an unrecognised kid.
Fix. Treat kid as an opaque key into a fixed, server-side map of known keys. Never interpolate it into a path or query, and reject any value not in the map.
jku, x5u and Embedded jwk Injection
How it works. The JWS spec defines header parameters that can carry or reference a key: jku and x5u are URLs to a key set or certificate, and jwk embeds a public key directly in the header. If a verifier fetches the key from a token-supplied jku URL, or trusts the embedded jwk, then the attacker chose the key that verifies their token, which defeats signing entirely. CVE-2018-0114 is a well-known instance: the Node.js node-joselibrary would, in affected versions, verify a token using a key embedded in the token itself.
Test. Confirm that setting jku to a URL you control, or embedding a jwk you generated, does not make the token verify. It must not.
Fix. Ignore token-supplied key material and key URLs. Configure the JWKS URL server-side and, if you must honour jku, restrict it to a strict allow-list of your own hosts. The safe default is that keys come only from configuration or a trusted, pinned jwks_uri; see what is JWKS.
Signatures That Are Never Verified (decode vs verify)
How it works. Decoding a JWT and verifying it are different operations. Decoding just Base64url-decodes the segments and gives you the claims; it proves nothing. A surprising amount of code reads claims from a decoded token and acts on them without ever checking the signature, for example calling a decode-only helper, or passing a "verify" option that is silently ignored. Anyone can then edit the payload and be believed. The distinction is covered in decoder vs validator.
Test. Change a single character in the payload of a real token and resend it. If the request still succeeds, some code path is decoding without verifying.
Fix. Verify on every request, with the key and an algorithm allow-list. Never make an authorisation decision from a merely decoded token. In JavaScript specifically, a decode-only library such as jwt-decode must never be used for trust; use a verifying library.
Missing exp, aud and iss Checks
How it works. A valid signature only proves the issuer produced the token. Without claim checks, an expired token still works (no exp check), a token minted for another application is accepted (no aud check), and a token from a different, possibly attacker-controlled issuer is trusted (no iss check). Audience confusion in particular lets a token intended for service A be replayed at service B.
Test. Replay an expired token, and a token whose aud names a different service, against your API. Both must be rejected. The mechanics are in the aud claim and JWT expiration time.
Fix. Require and validate exp, issand aud explicitly (most libraries take issuer and audience options that make these mandatory). Compare issand aud as exact strings.
Psychic Signatures (CVE-2022-21449)
How it works. This is a library bug, not a protocol flaw. Certain Java releases (15 through 18, before the April 2022 update) failed to reject an ECDSA signature whose rand s values were both zero, an obviously invalid signature that a correct implementation rejects outright. On an affected runtime, an all-zero signature verified against any ES256/384/512 token, so forgery was trivial for anyone who knew about it. The name "psychic signature" comes from a signature that verifies without knowing the key.
Test. Check the Java version running your verifiers. If it is in the affected range and below the April 2022 Critical Patch Update, it is vulnerable regardless of your JWT code.
Fix. Patch the JDK. This is a reminder that JWT security depends on the whole stack, not only your own validation logic, keep crypto libraries and runtimes current.
Token Leakage: URLs, Logs and localStorage + XSS
How it works. A bearer JWT is a credential: whoever holds it can use it until it expires. Tokens leak when they end up in a URL query string (written to server and proxy access logs, browser history and Referer headers), in application logs that dump full requests, or in a browser store readable by JavaScript. If a token is kept in localStorage, any cross-site scripting (XSS) flaw on the page can read and exfiltrate it, whereas an HttpOnly cookie cannot be read by script. The storage trade-offs are in where to store a JWT.
Test. Grep your logs and analytics for token-shaped strings (they start eyJ). Check that no route puts a token in the URL, and review whether your front end holds tokens where script can reach them.
Fix. Keep tokens out of URLs and logs; send them in the Authorizationheader or an HttpOnly cookie. Keep access-token lifetimes short so a leaked token expires quickly, and have a revocation path (see JWT logout and revocation). Fix XSS at the source with output encoding and a Content Security Policy; token storage choices only limit the blast radius.
JWT Attack Cheat Sheet: Test and Fix
| Vulnerability | How to test your system | Fix |
|---|---|---|
| alg: none | Confirm the verifier rejects a token whose header claims none | Allow-list algorithms; never let the token pick the check |
| Algorithm confusion | Confirm an RS256 service rejects an HS256 token | Pin one algorithm per key; pass typed key objects |
| Weak HMAC secret | Estimate secret entropy; audit how it was generated | 256-bit secret from a CSPRNG, or move to asymmetric keys |
| kid injection | Confirm an unusual kid value cannot change the lookup | Treat kid as a key into a fixed map, nothing more |
| jku / x5u injection | Confirm a token-supplied key URL is ignored | Configure the JWKS URL server-side; ignore header URLs |
| Embedded jwk | Confirm a key in the header is never trusted | Verify only against server-held keys |
| No signature check | Change one payload byte and confirm rejection | Call verify, not decode, on every request |
| Missing exp / aud / iss | Replay an expired or wrong-audience token | Require and check exp, iss and aud explicitly |
| Psychic signatures (CVE-2022-21449) | Check the Java runtime version | Patch affected Java to the April 2022 update or later |
| Token leakage | Search logs and analytics for tokens | Keep tokens out of URLs and logs; short lifetimes |
Scanners and Testing Tools
For systems you own, automated tools help confirm the fixes hold. jwt_tool (ticarpi/jwt_tool) is the widely used open-source toolkit: its playbook scan checks a target for the common misconfigurations above, and it can generate the test tokens (none, algorithm confusion, key injection) to confirm your verifier rejects them. Burp Suite's JWT extensions and the PortSwigger labs cover the same ground interactively. Use these against your own staging environment or a lab target, with authorisation, as a regression check that the vulnerabilities are closed, not as a way to attack third parties.
You can inspect any token's header and claims, and check alg, kid and exp, in the jwtdecode.app decoder, which runs entirely in your browser so the token is never uploaded. It decodes and (for HS, RS, PS and ES) verifies against a key you paste; it deliberately does not sign tokens or accept token-supplied keys, so it cannot be used to forge the attack tokens described here.
JWT Vulnerabilities in Java, Python and Express
The same classes of bug appear in every ecosystem, usually from a permissive default or a decode-only call:
- ·Node / Express (
jsonwebtoken): always passalgorithms: ['RS256'](or your one algorithm) tojwt.verify, and never make decisions fromjwt.decode. Setissuerandaudience. See JWT auth in Node.js. - ·Python (PyJWT):
jwt.decoderequires analgorithmslist and, by default, verifies the signature; setaudienceandissuerand do not disableverify_signature. - ·Java: pin the algorithm on the verifier rather than reading it from the token, keep the JDK patched (psychic signatures), and prefer libraries whose APIs take a typed key.
For the full defensive checklist across algorithms, keys, lifetimes and storage, see JWT security best practices.
Summary
JWT attacks almost all exploit a verifier that trusts the token too much: it lets the header pick the algorithm (none, algorithm confusion) or the key (kid, jku, jwk), skips the signature or the claim checks, or relies on a weak secret. The defences are consistent, pin one algorithm per key with an allow-list, take keys only from server-side configuration or a trusted JWKS, require and check exp, iss and aud, keep secrets strong and tokens out of URLs and logs, and patch the runtime. Test each with the corresponding check above against a system you own, and treat RFC 8725 and the OWASP cheat sheet as the source of truth.