Skip to content
Fast-turnaround security assessments available — 10+ years development & security experienceGet started
Back to Knowledge Base
vulnerabilityCWE-345OWASP A02:2021Typical severity: Critical

JWT Security: When Tokens Become Attack Vectors

·10 min read

JWT Security: When Tokens Become Attack Vectors

JSON Web Tokens have become the default credential format for modern APIs. Nearly every authentication system built in the last decade issues JWTs — signed compact objects that carry identity claims across HTTP requests without requiring server-side session state. The format is elegant and well-specified. The implementations, less so.

JWT vulnerabilities are not theoretical. They appear in real assessments with regularity, often granting complete authentication bypass or privilege escalation with minimal effort. The attack surface is well-defined and the defenses are straightforward, yet the same mistakes appear again and again in production systems.

What Is a JWT?

A JSON Web Token consists of three base64url-encoded sections separated by dots: a header, a payload, and a signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJyb2xlIjoidXNlciIsImV4cCI6MTcxMjAwMDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The header declares the token type and signing algorithm. The payload contains claims — statements about the subject, typically a user ID, roles, expiration time, and any application-specific data. The signature binds the header and payload together using a secret or private key.

json
// Decoded header
{ "alg": "HS256", "typ": "JWT" }
 
// Decoded payload
{ "sub": "123456", "role": "user", "exp": 1712000000 }

Because JWTs are self-contained and cryptographically signed, the server does not need to query a database on every request to validate the session. It simply verifies the signature and trusts the claims inside. This efficiency is the feature that makes JWT attacks so valuable to an attacker: forging the token means forging identity.

The alg:none Attack

The most brazen JWT attack requires no cryptographic capability at all. Some JWT libraries, when encountering a token with the algorithm header set to none, skip signature verification entirely — treating the unsigned token as valid.

The attack is simple:

  1. Obtain any valid JWT (your own session token, or a captured one)
  2. Decode the header and payload (they are just base64url-encoded JSON)
  3. Modify the payload to change your user ID, elevate your role, or extend expiration
  4. Re-encode with "alg": "none" in the header
  5. Append an empty signature (just a trailing dot) or no signature at all
  6. Submit the token

If the library is vulnerable, the server accepts it as if it were a legitimate signed token.

// Original
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJyb2xlIjoidXNlciJ9.signature

// Forged with alg:none
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiI5OTk5OTkiLCJyb2xlIjoiYWRtaW4ifQ.

This vulnerability persisted in widely-used JWT libraries for years. The fix is to explicitly reject any token whose algorithm field is not on an allowed list. A library that accepts none as a valid algorithm has no business being in a production authentication system.

Algorithm Confusion: Turning a Public Key Into a Secret

The algorithm confusion attack is more subtle but equally devastating. It exploits the interaction between asymmetric and symmetric signing algorithms.

When a server uses RS256 (RSA with SHA-256), it signs tokens with its private RSA key and verifies them with the corresponding public key. The public key is, by definition, public — it might be published at a JWKS endpoint, embedded in documentation, or extractable from any legitimate token.

The attack exploits a server that:

  1. Accepts the alg field from the token header without pinning it
  2. Has a JWT library that uses the same key for both RS256 verification and HS256 computation

An attacker who obtains the server's RSA public key can:

  1. Create a JWT with "alg": "HS256" instead of "RS256"
  2. Set any payload claims desired (elevated role, different user ID)
  3. Sign the token using HS256 with the public RSA key as the HMAC secret

When the server processes this token, a confused implementation uses the public key material to verify what it thinks is an HS256 signature. It is using the public key — which the attacker also has — as the HMAC secret. The verification succeeds, and the forged token is accepted.

python
import jwt
import base64
 
# Attacker has the server's RSA public key
with open('server_public_key.pem', 'rb') as f:
    public_key = f.read()
 
# Forge a token using the public key as an HMAC secret
forged_token = jwt.encode(
    {"sub": "admin_user_id", "role": "admin"},
    public_key,
    algorithm="HS256"
)

The defense is absolute: pin the algorithm on the server side. The server knows what algorithm it uses. It should reject any token that specifies a different one, regardless of what the client sends.

Weak HMAC Secrets

When JWTs are signed with HMAC (HS256, HS384, HS512), the security of every token issued by the system depends entirely on the strength of the secret key. And JWT secrets are brute-forceable offline.

Unlike password attacks that require interaction with a live server (and can be rate-limited), attacking a JWT secret requires only a captured token. The attacker takes the header and payload, tries candidate secrets, signs locally, and compares against the token's signature. No network interaction. No lockout. No logs.

A weak secret is found in seconds:

hashcat -a 0 -m 16500 captured_token.txt wordlist.txt

Common sources of weak JWT secrets found in assessments:

  • Default values from framework documentation (secret, your-256-bit-secret, changeme)
  • Application name or domain (myapp, api.example.com)
  • Environment variable names used as their own values (JWT_SECRET, SECRET_KEY)
  • Short random strings under 32 characters
  • Reuse of database passwords or other credentials

The minimum for an HMAC JWT secret is 256 bits of cryptographically random data — the same length as the hash output for HS256. Anything shorter provides meaningfully less security. Anything predictable provides none.

The kid Header: Key Identifier Injection

The kid (key ID) header parameter is an optional field that tells the server which key to use when verifying the token. It is intended for systems that rotate keys or support multiple signing keys simultaneously.

When an application uses kid to look up a key — from a database, a file system, or a key store — without sanitization, the parameter becomes an injection vector.

SQL Injection via kid

If the key lookup is implemented as a database query:

sql
SELECT key_value FROM signing_keys WHERE key_id = '<kid_value>';

An attacker can inject:

"kid": "anything' UNION SELECT 'attackercontrolledvalue'--"

The query returns attacker-controlled content as the signing key. The attacker then signs a forged token using that same value and the server accepts it.

Path Traversal via kid

If the key is loaded from a file:

python
key = open(f"/keys/{kid}").read()

An attacker can use path traversal:

"kid": "../../../../etc/passwd"

On Linux systems, the content of /etc/passwd is predictable. An attacker can sign a token using the contents of that file and the server will verify it successfully.

The defense is straightforward: never use attacker-controlled values in file paths or SQL queries without strict sanitization. Ideally, key IDs should be validated against an allowlist of known, expected values.

Missing Claim Validation

Even a correctly signed token can be dangerous if the application fails to validate its registered claims.

Expiration (exp): A token without expiration validation is valid forever. If a token is stolen, compromised, or issued to a since-deactivated user, it remains usable indefinitely. Every JWT verification should explicitly check the exp claim.

Audience (aud): The audience claim identifies the intended recipients of the token. A token issued for service A should not be accepted by service B. Without audience validation, a token obtained from one API can be replayed against another.

Issuer (iss): The issuer identifies who created the token. In multi-tenant or multi-service environments, a token from a different tenant's JWT issuer might be accepted if iss is not validated.

Not-before (nbf): Less commonly exploited but important for tokens that should only become valid at a future time.

In one assessment of a microservices platform, an access token issued by the authentication service for the user-facing API was accepted by the internal administration service because both services shared the same secret and neither validated the aud claim. A regular user's token, with role user, was usable against admin endpoints intended only for internal services.

JWT Libraries: Trust But Verify

The choice of library matters as much as the implementation. Several widely-used JWT libraries have had vulnerabilities including:

  • Accepting alg:none in production configurations
  • Using the wrong key for algorithm confusion scenarios
  • Parsing tokens before verifying them, enabling denial of service through malformed inputs
  • Accepting tokens with extremely large payloads

Using a well-maintained, actively-patched library is a baseline requirement. Libraries that have not been updated in years should be treated with suspicion regardless of their star count or download numbers.

Testing JWT Implementations

A systematic approach to JWT security testing:

  1. Capture a valid token and decode all three sections
  2. Try alg:none — modify the payload, set alg to none, strip the signature
  3. Try algorithm confusion — obtain the public key if asymmetric signing is in use, forge an HS256 token signed with it
  4. Extract and test the kid header — modify it to inject SQL or path traversal characters
  5. Brute-force the secret — run the token through common wordlists and patterns
  6. Test claim validation — submit expired tokens, tokens with wrong audience, tokens with wrong issuer
  7. Modify claims without changing the signature — a library that does not verify signatures before processing will accept these
  8. Test token replay — submit the same token to other services and endpoints

Many of these tests require only a captured token and a small script. None require prior knowledge of the server's keys or secrets.

Prevention

Pin the algorithm. The server knows what algorithm it expects. Reject any token that specifies a different one. Do not delegate this decision to the client.

Use strong secrets. Generate HMAC keys with a cryptographically secure random number generator. Minimum 256 bits. Store them securely, rotate them periodically, and never reuse them across environments.

Validate all registered claims. At minimum: exp (expiration), iss (issuer), aud (audience). Fail closed — if a claim is missing or invalid, reject the token.

Sanitize the kid header. If you must use it, validate it against an allowlist of known key IDs. Never interpolate it into SQL queries or file paths.

Use a well-maintained library. Check its security record. Ensure it is actively maintained. Read the documentation carefully for any non-default configuration required for security.

Rotate secrets. A compromised or cracked secret invalidates every token in circulation. Implement key rotation with version tracking so you can retire a secret without immediately logging out all users.

Key Takeaways

JWTs are cryptographically signed, but cryptography only helps if it is used correctly. The common attacks — alg:none, algorithm confusion, weak secrets, kid injection — all exploit the gap between what the JWT specification allows and what a secure implementation requires.

The defenses are well-understood and straightforward to implement. A server that pins its expected algorithm, uses a strong random secret, validates all claims, and sanitizes extensible parameters is not vulnerable to any of the standard JWT attack categories.

The vulnerability is not in the format itself. It is in implementations that trust the token's own claims about how it should be verified.

Need your authentication implementation reviewed? Get in touch.

Need your application tested?

We find these vulnerabilities in real applications every day. Get a comprehensive security assessment with detailed remediation.

Request an Assessment
jwtauthenticationcryptographyalgorithm-confusiontoken-forgeryapi-security

Summary

JSON Web Tokens are the dominant authentication mechanism for modern APIs, but implementation flaws turn them into attack vectors. Algorithm confusion, weak secrets, and missing validation allow attackers to forge tokens, escalate privileges, and bypass authentication entirely.

Key Takeaways

  • 1The alg:none attack exploits libraries that accept unsigned tokens when the algorithm header is set to 'none'
  • 2Algorithm confusion attacks trick servers into verifying HMAC signatures with a public RSA or ECDSA key, which attackers can access
  • 3Weak HMAC secrets can be brute-forced offline using only a captured token
  • 4The kid (key ID) parameter is injectable and can redirect key lookups to attacker-controlled content or SQL rows
  • 5Proper JWT validation requires pinning the expected algorithm, using strong secrets, and verifying all registered claims

Frequently Asked Questions

An algorithm confusion attack occurs when a server signs tokens with an asymmetric algorithm (like RS256) but the JWT library allows clients to specify the algorithm. An attacker changes the alg header to HS256 and signs the forged token using the server's public key as the HMAC secret. The server, confused about which key to use, verifies the HMAC signature with its public key and accepts the forged token.

Some JWT libraries treat a token with alg set to 'none' as an unsigned token that requires no signature verification. An attacker can take any valid JWT, modify the payload to escalate privileges, set alg to 'none', strip the signature, and submit it. Libraries that don't explicitly reject unsigned tokens will accept it as valid.

Yes. HMAC-signed JWTs (HS256, HS384, HS512) can be brute-forced offline without interacting with the server. A captured token is all an attacker needs. Short or predictable secrets — common words, application names, default framework values — fall quickly to dictionary attacks. The NIST minimum for HMAC secrets is 256 bits of entropy.

The kid (key ID) header parameter tells the server which key to use when verifying the token. If the application uses kid to construct a database query or file path without sanitization, an attacker can inject SQL to redirect key lookup to a controlled value, or use path traversal to point to a known file as the signing key.

Pin the expected algorithm — reject any token whose alg header does not match what you expect. Use a cryptographically strong secret of at least 256 bits for HMAC. Validate all registered claims: iss, aud, exp, nbf. Sanitize or ignore kid and other header parameters. Use a well-maintained library and keep it updated.