jwt.decode() in jsonwebtoken does not verify the signature — it base64-decodes the payload without checking the HMAC or RSA signature at all. An attacker can construct a JWT with any payload they want, including { role: 'admin', userId: '1' }, and your application will accept it as a valid authenticated identity. CWE-347 (Improper Verification of Cryptographic Signature) and OWASP A07 (Identification & Authentication Failures) both cite this exact pattern. Authentication built on jwt.decode() provides zero security — every token is trusted unconditionally.
Critical because using `.decode()` instead of `.verify()` makes JWT-based authentication trivially bypassable — any attacker can forge a token with arbitrary claims and gain full authenticated access.
Replace every jwt.decode(token) that feeds into user identity with jwt.verify(token, secret). The verify call will throw if the signature is invalid or the token is expired.
// Bad: trusts payload without signature check
import jwt from 'jsonwebtoken'
const payload = jwt.decode(token) // no signature verification!
req.user = payload
// Good: verify the signature before trusting the payload
import jwt from 'jsonwebtoken'
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!)
req.user = payload
} catch (err) {
return Response.json({ error: 'Invalid or expired token' }, { status: 401 })
}
.decode() is only safe when inspecting the token header (e.g., extracting the kid claim to select the right key) BEFORE a subsequent .verify() call. It must never be the final word on identity.
ID: ai-slop-security-theater.half-wired-crypto.jwt-verify-not-decode
Severity: critical
What to look for: When any JWT library is in package.json dependencies (jsonwebtoken, jose, @panva/jose, next-auth/jwt, jose-jwe-jws), walk source files for usages of the library. For each call site, count all occurrences of .decode( calls and .verify( calls. Before evaluating, extract and quote each .decode( and .verify( line. A .decode( call followed by use of the decoded payload (assigned to req.user, session, returned from a getCurrentUser, etc.) is the canonical "verify shortcut" anti-pattern. Count all files where .decode( is used to derive identity AND no corresponding .verify( is called in the same logical flow.
Detector snippet (shell-capable tools only): If rg is available, list every source file that calls .decode( on a JWT library. Exit 0 with output means at least one call site exists — proceed to prose to confirm the decoded payload is actually used as identity (and not verified elsewhere). Exit 1 with no output means no .decode( call sites — likely pass. Exit >=2 — fall back to prose reasoning.
rg -l -e "jwt\.decode\(" -e "jsonwebtoken['\"].*\.decode\(" src/
Pass criteria: 100% of JWT-derived identity assignments use .verify( (not .decode(). Report: "JWT library: [name]. X JWT call sites, Y use .verify(), 0 use .decode() to derive identity."
Fail criteria: At least 1 source file uses .decode( to extract user identity from a JWT without a corresponding .verify( call.
Do NOT pass when: .decode( is called with { complete: true } to inspect the header BEFORE a separate .verify( call — that's safe. The fail is .decode( followed by trusting the payload without .verify(.
Skip (N/A) when: No JWT library is in package.json dependencies.
Cross-reference: For broader authentication security analysis, the Security Hardening audit (security-hardening) covers JWT patterns in depth.
Detail on fail: "1 JWT decode-without-verify: src/lib/auth.ts line 12 calls jwt.decode(token) and assigns the payload to req.user without ever calling jwt.verify(). This is auth bypass — any forged JWT is accepted."
Remediation: jwt.decode() does NOT verify the signature — it just base64-decodes the payload. Anyone can forge a JWT and have it pass. Always use jwt.verify():
// Bad: decode trusts the payload
import jwt from 'jsonwebtoken'
const payload = jwt.decode(token) // no signature check!
req.user = payload
// Good: verify checks the signature
import jwt from 'jsonwebtoken'
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!)
req.user = payload
} catch (err) {
return Response.json({ error: 'Invalid token' }, { status: 401 })
}