Every JWT library ships two distinct functions: one that decodes a token (Base64URL decode + JSON parse), and one that verifies it (decode + signature check). The vulnerability appears when application code calls the decode-only function in a context that assumes the token has been validated. The signature is ignored entirely - the server processes whatever claims the token contains.
This sounds like an obvious mistake, but it recurs constantly because:
Libraries make decode functions convenient and prominently documented for debugging
Developers copy-paste example code that uses decode for simplicity
Microservice architectures create trust boundaries where "the gateway already verified it"
Testing always uses tokens created by the same server - so signature validity is always incidentally true
Vulnerable patterns by language
Node.js - jsonwebtoken
Vulnerable
const jwt =require('jsonwebtoken');// jwt.decode() returns the payload WITHOUT verifying the signature.// It accepts any well-formed JWT including forged ones.const decoded = jwt.decode(req.headers.authorization.split(' ')[1]);const userId = decoded.sub;// attacker controls this
Secure
const jwt =require('jsonwebtoken');// jwt.verify() throws if signature is invalid or token is expired.// Always pass an explicit algorithms array.const decoded = jwt.verify( req.headers.authorization.split(' ')[1],process.env.JWT_SECRET,{ algorithms:['HS256']});const userId = decoded.sub;// cryptographically proven
Python - PyJWT
Vulnerable
import jwt# options={"verify_signature": False} disables ALL verification.# This pattern is documented as "for debugging" but appears in production.payload = jwt.decode( token, options={"verify_signature": False})# Also vulnerable: algorithms=[] (empty list tricks some versions)
// Jwts.parserBuilder() without signedWith() → no signature checkClaims claims = Jwts.parserBuilder() .build() .parseClaimsJwt(token) // parseClaimsJwt, not parseClaimsJws .getBody();// Any unsigned or forged token passes
The most prevalent production variant is not a developer mistake on a single service, but an architectural assumption. A typical pattern:
Vulnerable microservice architecture
Client → API Gateway (verifies JWT) → Auth headers stripped ↓ Internal Service A Internal Service B ← also receives original JWT Internal Service C and re-decodes it without verify# The gateway verifies once. Internal services assume the gateway# already validated and call jwt.decode() for convenience.# An attacker with access to the internal network (or via SSRF)# can call internal services directly with forged tokens.
This pattern has been exploited in bug bounty programs targeting large SaaS platforms. The vulnerability surface is the internal services, not the public-facing gateway.
Zero-trust internal traffic
Internal services should verify JWT signatures even when behind an API gateway. A compromised upstream service, an SSRF vulnerability, or a misconfigured network route can all deliver attacker-controlled tokens to a service that skips verification.
Detection techniques
Testing for this vulnerability requires crafting a token with a tampered payload and an invalid signature, then observing whether the server accepts it.
Crafting a test token with a broken signature
importbase64, jsondef b64url(data):if isinstance(data, str): data = data.encode()returnbase64.urlsafe_b64encode(data).rstrip(b"=").decode()# Take an existing valid token and change a claimheader = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}))payload = b64url(json.dumps({"sub": "admin", "role": "admin", "exp": 9999999999}))fake_sig = b64url(b"invalidsignature")forged = f"{header}.{payload}.{fake_sig}"print(forged)# Send to server. If 200 → unverified signature.# If 401 → server validates signatures (good).
Real-world impact
Reported on HackerOne with payouts ranging $500-$25,000 depending on impact scope
Most critical when role, admin, or permissions claims control authorization decisions
Common in internal tooling built quickly without security review, then exposed via developer portals
Node.js jsonwebtoken: easy to call decode() vs verify() - accounts for the majority of bug reports in this category
Mitigations
Always call the verify function, never just decode - enforce this via code review or linting
Pass an explicit algorithms allowlist - reject tokens with unexpected algorithm values
Validate exp, aud, and iss claims after signature verification
Internal services must verify signatures independently - never trust "the gateway already checked it"
Consider a shared middleware library that enforces correct verification, removing the choice from individual service developers