JWT Security Vulnerabilities That Keep Showing Up in Audits
All articles
JWT securityAPI securityweb application securityauthentication vulnerabilitiesdeveloper security

JWT Security Vulnerabilities That Keep Showing Up in Audits

A breakdown of the JWT security vulnerabilities auditors find most often — algorithm confusion, weak secrets, storage mistakes — and how to fix each one.

WebSentry TeamAugust 10, 20267 min read

JSON Web Tokens are everywhere — single-page apps, mobile backends, microservices talking to each other. They're convenient because they're stateless and self-contained. That convenience is exactly why they get misused. A JWT is basically a signed piece of trust, and if you don't handle the signing, storage, or validation correctly, you've handed attackers a way to forge identity.

Below are the JWT vulnerabilities that show up again and again in real audits, along with the fixes that actually close them — not just theoretical advice.

1. The "alg: none" and Algorithm Confusion Attack

This is the classic JWT flaw and it's still found in production apps in 2024. The JWT header contains an alg field that tells the server which algorithm to use for signature verification. Some libraries, if misconfigured, will trust whatever algorithm the token claims to use.

How the attack works

  • An attacker takes a legitimate token, changes the header's alg to none, and strips the signature.
  • If the server's verification logic doesn't explicitly reject unsigned tokens, it accepts the forged token as valid.
  • A related variant: if you sign with RS256 (asymmetric) but the server accepts HS256 (symmetric), an attacker can take your public key, use it as the HMAC secret, and sign their own tokens.

The fix

  • Never let the algorithm be dictated by the token. Hardcode the expected algorithm on the server side and reject anything else.
  • In most JWT libraries this means passing an explicit algorithms: ['RS256'] allow-list to the verify function rather than trusting the decoded header.
  • Ban none as an algorithm at the library/config level, not just in application logic.

2. Weak or Guessable Signing Secrets

HS256-signed tokens are only as strong as the secret used to sign them. A shockingly common finding: a secret that's short, a dictionary word, or copy-pasted from a tutorial ("mysecretkey123").

Why it matters

If an attacker can brute-force or guess your HMAC secret offline, they can forge any token they want — including one claiming to be an admin. Tools like hashcat have JWT-cracking modes built specifically for this.

The fix

  1. Use a cryptographically random secret of at least 256 bits (32+ bytes) for HS256.
  2. Better yet, move to RS256 or ES256 (asymmetric signing) so the verification key is public and the private signing key never leaves your auth server.
  3. Store secrets in a proper secrets manager (Vault, AWS Secrets Manager, environment-injected at deploy time) — never hardcoded in source or committed to Git.
  4. Rotate signing keys periodically and support key rotation via a kid (key ID) header so old tokens can still validate during transition windows.

3. Missing or Ignored Expiration Claims

JWTs without an exp claim — or with one that's set absurdly far in the future — are effectively permanent credentials. If one leaks (browser history, log file, referrer header, a misconfigured analytics tool), it's valid forever.

The fix

  • Always set a short exp — 15 minutes is a reasonable default for access tokens.
  • Use a separate, longer-lived refresh token stored more securely (httpOnly cookie, not localStorage) to issue new access tokens.
  • Validate exp server-side on every request — don't rely on the client to "know" its token expired.

4. No Real Revocation Strategy

This is the tradeoff nobody wants to talk about: JWTs are stateless by design, which means there's no built-in way to revoke one before it expires. If a user logs out, changes their password, or gets flagged for compromise, their old token is often still perfectly valid.

The fix

  • Keep access token lifetimes short (see above) so the revocation window is naturally small.
  • Maintain a denylist (or allowlist) of revoked token IDs (jti claim) in Redis or similar, checked on each request for sensitive actions.
  • On password change or logout-everywhere events, increment a "token version" stored per-user and embed it as a claim — reject tokens with a stale version.

5. Storing JWTs Insecurely on the Client

Where you store the token matters as much as how it's signed.

Common mistakes

  • Storing tokens in localStorage or sessionStorage, both fully accessible to JavaScript — meaning any XSS vulnerability becomes instant token theft.
  • Storing tokens in cookies without the HttpOnly, Secure, and SameSite attributes set correctly.

The fix

  1. Store tokens in cookies with HttpOnly (blocks JS access), Secure (HTTPS only), and SameSite=Strict or Lax (limits CSRF exposure).
  2. If you must use JS-accessible storage for architectural reasons, pair it with a strong Content Security Policy to reduce XSS blast radius — and audit your CSP regularly, because a loose script-src defeats the purpose entirely.
  3. Never put JWTs in URL query strings — they end up in server logs, browser history, and referrer headers.

6. Sensitive Data Sitting in the Payload

JWTs are encoded, not encrypted. Anyone with the token can base64-decode the payload and read it in plain text — try it yourself at jwt.io.

The fix

  • Never put passwords, SSNs, full names, emails (if avoidable), or internal permission structures directly in the payload.
  • Keep the payload to an opaque user ID and minimal role/scope claims. Look up sensitive details server-side when needed.
  • If you genuinely need encrypted claims, use JWE (JSON Web Encryption) instead of plain JWT.

7. Improper Signature Verification Order

A subtler bug: some implementations decode the payload and act on its contents before verifying the signature. This opens the door to injection-style attacks where a forged payload gets processed before it's ever validated.

The fix

  • Always verify the signature first, as an atomic step, before touching any claim in the payload.
  • Use well-maintained libraries (jsonwebtoken, jose, PyJWT) rather than rolling your own decode/verify logic — and keep them updated, since several past CVEs in JWT libraries stemmed from exactly this kind of ordering bug.

Checking Your Broader Security Posture

JWT handling is one piece of a much bigger picture. A forged token is less dangerous if your headers, CORS policy, and cookie flags are also locked down correctly — and more dangerous if they're not. For example, a loose CORS configuration can let a malicious origin make authenticated requests using a stolen token, and missing security headers make XSS-based token theft easier in the first place.

This is where a tool like WebSentry is useful for catching the surrounding gaps: it checks SSL configuration, security headers, CSP strength, cookie flags, DNS setup, and CORS policy, and rolls it all into an A–F grade so you can see at a glance where the weak points are — the same weak points that turn a JWT bug into an actual breach.

A Quick Audit Checklist

  1. Algorithm is hardcoded and allow-listed server-side — never trusted from the token header
  2. Signing secret is 256-bit random, or you've moved to RS256/ES256
  3. Access tokens expire in 15 minutes or less
  4. Refresh tokens are stored in HttpOnly, Secure, SameSite cookies
  5. A revocation mechanism exists (denylist, token versioning)
  6. Payload contains no sensitive data
  7. Signature is verified before any claim is read
  8. Libraries are current and patched against known CVEs

If you're running JWT-based auth in production, run a free scan at websentry.dev to see how your headers, CORS, CSP, and cookie configuration stack up around it — because the token is only as secure as the environment it's issued into.

Check your own site

Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.