Web Security for Frontend Devs · Part 15 — JWT & Token Attacks
Advanced track: JWT attacks to recognize — alg:none, RS256→HS256 confusion, weak HMAC secrets, kid/jku injection, unchecked exp/aud/iss — why the browser must never trust a claim. With a forge/verify simulator and exercises.
Phần 15 — Nhánh nâng cao trong series Web Security for Frontend Devs. Trước:
Phần 5 nói lưu token ở đâu. Phần nâng cao này nói token bị giả mạo và lạm dụng thế nào. Hầu hết lỗi JWT là sai sót verify trên server, nhưng frontend thường là người đọc claim, chuyển token giữa cửa sổ (Phần 14), và review code auth — nên bạn phải nhận ra các mẫu này.
JWT trong 30 giây
JWT gồm ba phần base64url:
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIxIiwicm9sZSI6InVzZXIifQ . 3a8f…sig
{"alg":"HS256"} {"sub":"1","role":"user"} HMAC(header.payload, secret)
Hai sự thật chi phối mọi tấn công:
- Header và payload không bí mật — ai cũng decode được.
- Chỉ chữ ký khiến token đáng tin, và chỉ khi server verify đúng — đúng thuật toán và đúng khóa.
Mọi tấn công dưới đây khiến server nhận token đáng lẽ phải từ chối.
Tấn công 1 — alg: none
Spec JWT định nghĩa thuật toán none “không bảo mật” — token không chữ ký. Verifier ngây thơ tin alg trong header sẽ bỏ qua kiểm chữ ký:
header = {"alg":"none","typ":"JWT"}
payload = {"sub":"1","role":"admin"} ← attacker sets whatever they want
token = base64url(header) + "." + base64url(payload) + "." ← empty signature
Nếu được nhận, kẻ tấn công trở thành role: admin với token tự viết tay. Sửa: ghim thuật toán mong đợi; đừng để header token chọn.
Tấn công 2 — nhầm thuật toán RS256 → HS256
RS256 ký bằng khóa riêng và verify bằng khóa công khai (vốn công khai). Nếu server gọi verify(token, key) chung và để token chọn thuật toán, kẻ tấn công đổi alg sang HS256 và ký bằng khóa công khai làm secret HMAC:
1. attacker fetches the server's RS256 PUBLIC key (often at /.well-known/jwks.json)
2. crafts {"alg":"HS256"} with chosen claims
3. signs: HMAC-SHA256(header.payload, PUBLIC_KEY_BYTES)
4. server verifies HS256 using the public key it has on hand → ✓ valid
Server tưởng đang verify chữ ký RSA; thực ra tính HMAC với khóa kẻ tấn công cũng biết. Sửa: ghim RS256 rõ và từ chối khác; đừng đưa khóa công khai vào verifier HMAC.
Tấn công 3 — secret HMAC yếu
An toàn HS256 hoàn toàn nằm ở entropy của secret. Secret như secret, changeme, hay .env rò rỉ có thể brute-force offline: kẻ tấn công có token đã ký, chạy wordlist đến khi HMAC khớp, rồi giả mạo token tùy ý. Sửa: dùng secret dài, ngẫu nhiên (≥ 256 bit) từ secrets manager; xoay khi rò; không commit (Phần 9).
Tấn công 4 — tiêm header kid / jku / x5u
Header JOSE có thể bảo verifier dùng khóa nào:
kidthường dùng tra khóa — nếu nối vào path file hay SQL, mở path traversal hoặc SQLi để kiểm soát khóa verify.jku/x5utrỏ URL key set — nếu server fetch không allowlist, kẻ tấn công host khóa của họ và ký token trông hợp lệ (cũng là SSRF).
Sửa: coi mọi tham chiếu khóa từ header là không tin cậy — allowlist kid, không dùng trong path/query, chỉ fetch khóa từ endpoint ghim sẵn.
Tấn công 5 — không kiểm exp / nbf / aud / iss
Chữ ký hợp lệ không nghĩa là phiên hợp lệ. Server verify chữ ký nhưng bỏ kiểm claim sẽ nhận:
- token hết hạn — replay sau logout.
- token sai audience/issuer — token của dịch vụ A dùng lại ở B.
Sửa: luôn validate exp, nbf, aud, iss theo giá trị mong đợi, với sai số đồng hồ nhỏ.
Góc frontend: đừng tin claim trong trình duyệt
Decode JWT phía client để hiện tên hay logout sớm gần exp thì ổn — đó là UX. Nhưng trình duyệt bị user sửa được (Phần 10), nên:
// ❌ security decision in the browser — trivially bypassed in DevTools
const { role } = jwtDecode(token);
if (role === 'admin') showAdminPanel(); // attacker just edits the decoded object
// ✅ client decode is UX only; the SERVER authorizes every privileged action
const { name, exp } = jwtDecode<{ name: string; exp: number }>(token);
renderGreeting(name);
if (Date.now() / 1000 > exp - 30) scheduleSilentRefresh();
// admin actions call an API that re-verifies the token and checks role server-side
Cũng: không để secret trong payload JWT (đọc được), và không log token đầy đủ — chúng là credential bearer.
Thử ngay — trình mô phỏng giả mạo & verify JWT
Bắt đầu từ token mẫu, áp dụng tấn công, rồi cấu hình verifier và xem token giả mạo được nhận hay từ chối — và vì sao.
Mở demo đầy đủ:
Checklist phòng tránh
- Ghim thuật toán; từ chối
nonevàalgbất ngờ. - Dùng đúng loại khóa; không verify HMAC bằng khóa công khai.
- Secret HMAC mạnh, có xoay; ngoài repo (Phần 9).
- Coi
kid/jku/x5ukhông tin cậy: allowlist, không path/SQL, endpoint ghim. - Validate
exp/nbf/aud/issmỗi request. - Decode client chỉ UX — server authorize; không secret trong payload; không log token.
Bài tập / Exercises
1. Verifier gọi jwt.verify(token, publicKey) không chỉ thuật toán, dùng cặp khóa RS256. Giải thích cách giả mạo và cách sửa một-tuỳ-chọn.
Lời giải
Kẻ tấn công đặt alg: HS256 và ký bằng byte khóa công khai làm secret HMAC; thư viện “verify với publicKey” tính HMAC và khớp. Sửa: truyền { algorithms: ['RS256'] } để thư viện từ chối token HS256.
2. Vì sao decode role trong React để chặn route admin là không đủ, kể cả khi JWT ký đúng?
Lời giải
Guard chạy trong trình duyệt mà user kiểm soát hoàn toàn. Ẩn UI ổn cho UX, nhưng API sau hành động admin phải verify lại token và kiểm role phía server. Nếu không thì là kiểm soát truy cập trên client = không có kiểm soát.
3. Team dùng HS256 với secret "prod-secret-2021". Nêu hai điều sai và cách khắc phục.
Lời giải
Entropy thấp — secret đoán được, brute-force offline. Trông như commit/sống lâu và bị chia sẻ. Thay bằng secret ngẫu nhiên ≥ 256-bit từ secrets manager, xoay (vô hiệu token cũ), và xóa khỏi repo/history.
Nâng cao:Trong simulator, tạo token alg:none cấp role: admin, cho qua verifier “tin header alg”, rồi đổi sang “ghim HS256” và xác nhận bị từ chối.
Điểm chính
- JWT đáng tin chỉ khi verify đúng thuật toán và khóa — header/payload công khai.
alg:nonevà nhầm RS256→HS256 đều do để token chọn thuật toán — hãy ghim.- Secret HMAC yếu và tiêm
kid/jkutrao khóa cho kẻ tấn công. - Luôn validate
exp/nbf/aud/iss. - Trong trình duyệt, decode JWT chỉ UX; server quyết định mọi authorization.
Tiếp theo
Nhánh nâng cao khép lại với Web Cache Deception & Open-Redirect Chaining — cách tầng cache và tin URL redirect làm rò phản hồi riêng tư và chuỗi thành chiếm tài khoản.