Introduction
JSON Web Token (JWT) is widely used for authentication and authorization in web APIs, mobile applications, and distributed systems. It is commonly implemented in standards such as OAuth 2.0 and OpenID Connect.
However, JWT is not inherently secure. While it offers powerful cryptographic mechanisms, improper implementation and design flaws introduce security vulnerabilities that can lead to API breaches, token tampering, and authentication bypass.
This article:
- Explore how JWT works and its structure.
- Analyze common security vulnerabilities in JWT implementations.
- Demonstrate how attackers exploit these flaws.
- Shows best practices to improve JWT security.
What is a JSON Web Token?
A JWT is a compact, URL-safe token format that represents claims between two parties. It consists of three base64-encoded components:
- Header – contains metadata about the token, including the signing algorithm (e.g., HS256, RS256).
- Payload – contains the claims (data) about the user, such as user ID, role, and expiration time.
- Signature – Ensures integrity of the token using a secret key.
Attention: JWTs are NOT encrypted by default—only signed. This means the payload can be read by anyone who intercepts the token.
At jwt.io you can see how it all looks like at practicce:

Common JWT Vulnerabilities and Exploits
1. Timing attack In JWT/JWS context
A timing attack is a side-channel attack that works by measuring how long a system takes to perform certain operations, and using that time difference to guess secret information (like passwords, keys, or signatures).
Many libraries implement naive string comparison for checking the token’s signature:
if ($expected_signature === $provided_signature) {
// Token is valid
}
This comparison is done byte-by-byte, and stops as soon as a mismatch is found. So if the first 5 bytes match, but the 6th does not, it takes longer to verify than if the first byte already mismatched. This is called a “byte-wise timing leak” and can allow signature forgery even without knowing the secret. According to a cited study, with a high number of requests (about 55 per second in lab conditions), it’s possible to guess a valid signature in 22 hours. Under ideal conditions (low network noise), fewer requests are needed.
To prevent this:
- Use constant-time comparison functions to prevent timing leaks, e.g hash_equals()
- Limit brute-force attempts by implementing: rate limiting, IP blocking, logging failed signature validations
- Use strong secrets – even if timing attack is attempted, brute-forcing a strong HMAC key (e.g., 256-bit random string) is still infeasible.
2. The “None” Algorithm Attack
- JWT allows “alg”: “none”, meaning no signature is required.
- Attackers can change the algorithm to “none” and modify the payload freely.
Example of a tampered header:
{
"alg": "none",
"typ": "JWT"
}
If a server blindly accepts unsigned tokens, an attacker can create valid-looking JWTs without needing the secret key.
Fix: Enforce signature validation and disable “none” algorithm.
3. Weak Secret Keys (HMAC Brute Force)
- JWTs signed with weak or common secrets (e.g., “secret”, “password”) can be brute-forced.
- Attackers can use hashcat or JWT-cracking tools to recover weak HMAC keys.
Example attack command:
hashcat -m 16500 jwt_hashes.txt -a 3 ?1?1?1?1?1?1?1?1
Fix: Use long, random secrets (at least 256-bit for HS256).
4. Algorithm Confusion Attack (HS256 to RS256)
Some JWT implementations allow switching signing algorithms.
- Attackers can change “alg”: “HS256” to “alg”: “RS256” and use a public key as a secret to forge a signature.
Fix: Always validate the algorithm server-side and do not allow algorithm changes.
5. Token Disclosure in Error Messages
- Some APIs return detailed error messages containing the correct signature.
- Attackers can modify the JWT payload, send an invalid token, and receive a leakage of valid tokens in debug responses.
Example response:
Invalid signature. Expected: S2LYDA02AnNSqpJDWIJfqFXmEuAwRi8E19HQRT5KM
Fix: Avoid exposing sensitive error messages, especially in production.
6. Long-Lived Tokens Without Expiry
- Some JWTs never expire, allowing persistent access even after revocation.
- If a token is stolen, it can be used indefinitely.
Example payload:
{
"sub": "123456",
"name": "admin",
"exp": 9999999999
}
Fix: Always set a reasonable expiration time and implement token revocation mechanisms.
7. Storing JWTs in the URL
- Some applications mistakenly pass JWTs in URLs, leading to exposure in browser history and server logs.
Example:
https://example.com/dashboard?token=eyJhbGciOiJIUz...
Fix: Always send JWTs in HTTP headers (e.g., Authorization: Bearer ).
8. Insufficient Audience (aud) and Issuer (iss) Validation
- If an API accepts any token without verifying its aud and iss, attackers can reuse tokens from another system.
Example of a malicious token:
{
"iss": "malicious-issuer",
"aud": "api.example.com"
}
Fix: Always validate issuer (iss) and intended audience (aud).
Best Practices for JWT Security
- Use strong secret keys (at least 256-bit for HS256, 2048-bit for RSA keys).
- Disable “none” algorithm explicitly in JWT libraries.
- Enforce a single signing algorithm (do not allow user-specified algorithms).
- Set short token lifetimes (exp) to minimize risk of misuse.
- Implement token revocation (e.g., store revoked tokens in a blacklist).
- Verify aud and iss claims to prevent cross-system token reuse.
- Avoid sensitive data in JWT payloads (JWTs are not encrypted by default).
- Store JWTs securely (not in localStorage or URLs.
- Use JWE (JSON Web Encryption) if data confidentiality is required.
- Monitor and update JWT libraries regularly to patch vulnerabilities.
- Implement best amti timing attack practices
If you would like to get real practice at JWT vulnerabilities and how to prevent them – then welcome to my Udemy course: “PHP REST API cybersecurity“