JWT (JSON Web Token) is widely used in authentication and session management for modern SPAs and microservices. That's exactly why getting JWT verification wrong in one place collapses authentication entirely. As PortSwigger says, "the security of any JWT-based mechanism depends heavily on the cryptographic signature," and most attacks exploit cut corners in that signature verification. This article explains those attack techniques faithfully to the official source.
An absolute premise: all procedures only within a legal lab or an authorized scope. Forging or impersonating others' tokens is unauthorized access if done without authorization, even for verification purposes (→ the legal guide). The map is the pillar. For the basics of token design including JWT, also see the difference between ID tokens and access tokens.
1. The structure of a JWT — three parts
A JWT is header.payload.signature, three parts base64url-encoded and dot-separated.
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiJ3aWVuZXIifQ . <署名>
└─ header ────────┘ └─ payload ──────────┘
{"alg":"HS256"} {"sub":"wiener","role":"user"}
- header: meta info like the signing algorithm (
alg). - payload: claims (who, permissions, expiry, etc.). Base64 is not encryption, so anyone can read the contents.
- signature: the header+payload signed with a key. Tamper detection relies on this alone.
In other words, if signature verification is lax, you can rewrite the payload to "role":"admin" at will.
2. Exploiting signature-verification flaws
2.1 Not verifying the signature / accepting an arbitrary signature
A case where the developer confuses "decode" and "verify" and trusts the payload without verifying the signature. It passes just by rewriting the payload.
# payload の "sub":"wiener" を "sub":"administrator" に書き換えて送るだけで通る
# (Burp の JWT Editor 拡張でワンクリック改ざん→送信できる)
2.2 alg:none
The JWS spec has "no signature (alg:none)." If the server accepts this, you can empty the signature and freely tamper with the payload.
# header を {"alg":"none"} にし、署名部分を空にする
eyJhbGciOiJub25lIn0 . eyJzdWIiOiJhZG1pbmlzdHJhdG9yIn0 .
└─ 署名は空
Even if a simple none is rejected, as PortSwigger notes, obfuscation like None, nOnE, or unexpected encodings can slip past the verification filter.
3. Brute-forcing a weak secret key (HS256)
HMAC-family (HS256) uses "an arbitrary string" as the secret key. If the key is weak/default, it can be cracked with hashcat.
# JWTを辞書攻撃(-m 16500 が JWT モード)。割れた鍵が標準出力に出る
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/jwt.secrets.list
# 鍵が判明したら、任意のpayloadを「正規に」署名できる = 完全な認証バイパス
# 例: {"sub":"administrator"} を割れた鍵で署名して送る
Crack the key and it's over. So the secret key must be sufficiently long and random (reusing weak default keys is the typical incident). For the principles of applied cryptography and key management, see the password-hashing/cryptography/key-management guide.
4. Header-parameter injection (jwk / jku / kid)
The JWT spec has mechanisms to specify, in the header, the key used for signature verification. If the server trusts these without verification, you get it to verify with the attacker's key.
| Parameter | Meaning | Attack |
|---|---|---|
| jwk | Embed a public key directly in the token | Sign with your own key pair and embed the public key in jwk |
| jku | Point to the URL of a key set (JWK Set) | Specify the attacker's server's JWK Set in the URL |
| kid | The ID of the key to use (often used for a file/DB reference) | Manipulate the key via path traversal (../../dev/null) or SQLi |
# jwk 注入の例(header に自前の公開鍵を埋め、その秘密鍵で署名)
{"alg":"RS256","typ":"JWT","jwk":{"kty":"RSA","n":"<攻撃者の公開鍵>","e":"AQAB"}}
# kid をパストラバーサルに:既知内容のファイルを鍵に使わせ、署名を予測可能にする
{"alg":"HS256","kid":"../../../../../../dev/null"}
# /dev/null は空 → 鍵が空文字列になり、攻撃者が署名を作れる
5. Algorithm confusion (key confusion) attack — RS256 → HS256
The most cunning is algorithm confusion / key confusion. It succeeds when the server assumes RS256 (asymmetric: verify with the public key, sign with the private key) but doesn't pin the alg at verification time.
The mechanism:
- The RS256 public key is obtainable by anyone (from a JWK endpoint or certificate).
- The attacker makes a token with the
algchanged to HS256 (symmetric). - They sign that token using the public-key string as the "HMAC secret key."
- The server judges "it's HS256" and verifies with its public key as the HMAC key → it matches.
# 概念:本来 RS256 のサーバーへ、HS256 で署名したトークンを送る
# 署名鍵 = サーバーのRSA「公開」鍵(PEM文字列そのもの)
header = {"alg":"HS256"}
payload = {"sub":"administrator"}
signature = HMAC-SHA256(base64(header)+"."+base64(payload), <RSA公開鍵PEM>)
Since the public key isn't secret, the attacker can forge a token that "looks" legitimate. This is the terror of key confusion (can be PoC'd with Burp's JWT Editor).
6. [Defender side] Root-cause defenses
Once you understand the attack structure, the defense is clear. It follows PortSwigger's recommendations.
// ✅ 検証時にアルゴリズムを「固定」する(最重要)。none も他algも受け付けない
import { jwtVerify } from "jose";
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"], // ← 許可リスト。これが無いと alg 混同/none を許す
issuer: "https://auth.example",
audience: "https://api.example",
});
Design principles:
- Pin the algorithm server-side (allowlist). Reject
alg:noneand don't trust the token'salg. This alone blocks most alg confusion and none. - Always verify: don't confuse the "decode" function (
decode) with the "verify" function (verify). - Allowlist the hosts for jku/jwk (don't trust keys at arbitrary URLs). Apply path-traversal/SQLi countermeasures for kid.
- Make the secret key long and random (avoid weak HS256 keys). Design key rotation.
- Use a proven, up-to-date library per its documentation (don't roll your own).
jose/jsonwebtoken, etc. - Expiry (exp) and revocation: short expiry + refresh tokens + a revocation list limit the damage on leakage.
- Storage location: to avoid XSS theft, consider an
HttpOnlycookie in the browser (→ XSS defense).
Robust design as an authentication platform (selecting Cognito/Auth0/Clerk/Supabase Auth, and RS256 JWKS verification) is detailed in authentication-platform selection and Cognito JWT RS256 verification.
7. Summary
- JWT's security fully depends on signature verification: cut corners (unverified, alg:none, lax verification) are the main holes.
- Weak keys can be cracked with hashcat: make HS256 keys long and random.
- Header injection: if the server trusts jwk/jku/kid without verification, it passes with the attacker's key.
- Algorithm confusion: with the alg unpinned, the public key gets used as the HMAC key and tokens are forged.
- Root-cause defense: algorithm pinning + always verify + strict jku/kid + long keys + up-to-date library + exp/revocation.
Next, head to the complete conquest of authentication vulnerabilities, which targets the login mechanism itself.