Skip to content

JWT vulnerabilities

CWE-347OWASP A07:2021Updated August 31, 20266 min read

JWT vulnerabilities are flaws in how an application signs or verifies a JSON Web Token, letting an attacker forge one and become any user. The common cases are accepting alg:none, HS256/RS256 confusion, skipping the signature check and ignoring the exp, aud and iss claims. The fix is to pin one algorithm, verify every claim and keep lifetimes short.

A JSON Web Token rides along on every request between the browser and your API, and your server trusts whatever it reads from that token: who the user is, which role they hold, when the session ends. That trust only holds while the signature is checked correctly. When it is not, an attacker rewrites the token and becomes anyone they like. This article walks through the common JWT mistakes and how to close them.

What are JWT vulnerabilities?

JWT vulnerabilities are flaws in the way an application issues or verifies a JSON Web Token (JWT), a compact, signed record of a user’s identity and claims that the server hands out at login and trusts on every request that follows. A token has three parts separated by dots: a header that names the signing algorithm, a payload that carries the claims, and a signature over the first two parts. The signature is the only thing that ties the token to your server. Break its verification and the claims become editable.

Think of a JWT as a festival wristband. The colour and the hologram prove that a guard issued it, so you walk past the gate without showing ID again. That only works if the guard actually inspects the hologram. A JWT vulnerability is the guard who glances at the colour and waves everyone through, or who accepts a wristband a visitor coloured in themselves.

The mistakes cluster into a handful of patterns: accepting an algorithm of none, confusing a symmetric algorithm with an asymmetric one, decoding a token without verifying its signature at all, and verifying the signature but never checking the standard claims such as expiry, audience and issuer. A separate mistake is treating the payload as private and storing secrets in it.

How does a JWT attack work?

Take an API that reads the current user straight from the token on each request. The developer verifies the signature but leaves the details to the library.

Vulnerable:

const jwt = require("jsonwebtoken");
const fs = require("node:fs");
const publicKey = fs.readFileSync("public.pem");

// Vulnerable: the algorithm is taken from the token header,
// and no issuer, audience or expiry is enforced.
function currentUser(token) {
  return jwt.verify(token, publicKey);
}

Because the accepted algorithms are not pinned, the verifier trusts whatever the header claims. Two forgeries follow from that single omission.

The first is the alg:none attack. An attacker sets the header algorithm to none, edits the payload to grant themselves an admin role, and sends an empty signature. A verifier that honours none skips the signature check and returns the forged claims. On the wire the request looks like this, with the trailing dot marking the empty signature:

GET /account HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMDAxIiwicm9sZSI6ImFkbWluIn0.

Decoded, the header reads {"alg":"none"} and the payload reads {"sub":"1001","role":"admin"}. Nothing signed it, and nothing needed to.

The second is HS256/RS256 confusion. Your server signs with an RSA private key and verifies with the matching public key, which by design is not secret. An attacker takes that public key, signs a token with HMAC-SHA256 using the public key bytes as the shared secret, and sets the header to HS256. A verifier that picks the algorithm from the header runs HMAC with the public key as the secret, the very value the attacker used, so the signature matches. The asymmetric scheme collapses into a symmetric one keyed on a public value.

The most basic mistake is simpler still: decoding without verifying at all. A call like jwt.decode(token) reads the claims and never touches the signature, so any token the attacker types is accepted.

The fix pins one algorithm and checks the claims the token carries.

Secure:

const jwt = require("jsonwebtoken");
const fs = require("node:fs");
const publicKey = fs.readFileSync("public.pem");

// Secure: one algorithm is pinned and the standard claims are enforced.
function currentUser(token) {
  return jwt.verify(token, publicKey, {
    algorithms: ["RS256"],
    issuer: "https://auth.example.com",
    audience: "https://api.example.com",
    maxAge: "15m",
  });
}

Pinning algorithms to ["RS256"] rejects both none and any HS256 forgery, because HMAC is no longer an algorithm the verifier will run. The issuer and audience options bind the token to your service, so a valid token minted for a different application is refused. The maxAge option, together with the exp claim, keeps a stolen token useful only briefly.

A signed JWT is not encrypted. The header and payload are only base64url-encoded, which means anyone holding the token can read them. Never place passwords, API keys or personal data you would not print on a postcard in the payload; if the contents must stay secret, use an encrypted token (JWE) instead.

What is the impact of JWT vulnerabilities?

A JWT is a bearer credential the whole API trusts, so a single verification flaw is an authentication bypass on every endpoint at once. An attacker who forges a token sets any sub and any role they want: they read and change another user’s data, and they escalate from a normal account to an administrator. Where the application trusts the token for authorization decisions, this is broken access control delivered through the front door.

The business impact runs from high to critical. Full account takeover, theft of regulated personal data with a notification duty, and tampering with records all sit at the top of that range. If the payload also carries secrets, the exposure widens to information disclosure, because the token was readable all along. A signed JWT cannot be revoked on its own once issued, so a forged or stolen token stays valid until it expires, which is exactly why a short lifetime matters.

How do you detect JWT vulnerabilities?

Start by decoding a real token: split it on the dots and base64url-decode the first two parts. Read the header algorithm and the payload claims, and note whether exp, aud and iss are present and enforced. A token that never expires, or one issued for a different audience yet accepted here, is already a finding.

Then test the verifier directly. Change the header algorithm to none and strip the signature. Re-sign an RS256 token with HS256 using the public key as the secret. Tamper with a single claim and see whether the change is accepted. Remove or backdate exp and replay an old token. Tooling such as the Burp Suite JWT Editor extension or the open-source jwt_tool automates each of these probes.

Scanners flag the obvious cases, but the interesting ones sit behind login and depend on how each endpoint reads the token. AssistSec examines JWT issuance and verification as part of a penetration test and shows, per finding, the exact forged token that the application accepted.

How do you prevent JWT vulnerabilities?

  • Pin the algorithm. Pass an explicit allowlist of one algorithm to the verifier, for example algorithms: ["RS256"], and never accept none or let the token choose.
  • Verify every standard claim. Enforce exp and nbf for timing, and check aud and iss so a token minted elsewhere cannot be replayed against your service.
  • Keep lifetimes short. Give access tokens minutes, not days, and use a separate refresh token for longer sessions so a leaked token expires quickly.
  • Protect and separate the keys. Use a long, random secret for HS256, keep private keys out of the repository, and rotate keys with a published kid so you can retire a compromised one.
  • Never store secrets in the payload. Treat the token as public data, and reach for an encrypted token (JWE) when the contents genuinely must stay hidden.
  • Use a maintained library and keep it current. Rely on a vetted implementation rather than hand-rolled parsing, and update it, since older versions of popular libraries accepted none by default.

Sources

Frequently asked questions

Is a JWT encrypted?

No, not by default. A standard signed JWT is only base64url-encoded, so anyone who holds the token can read the header and the payload. The signature proves the token was not changed, but it does not hide the contents. If you need the payload to be secret, use an encrypted token (JWE) and never place passwords or keys in a plain signed JWT.

What is the alg:none attack?

A JWT header names the algorithm used for the signature. If a verifier honours an algorithm of none, it skips the signature check entirely. An attacker then sets the header to alg:none, edits the payload freely, sends an empty signature and is accepted as whatever the payload claims. The defence is to pin the accepted algorithm and never allow none.

How does HS256/RS256 confusion work?

With RS256 the server signs with a private key and verifies with the public key, which is not secret. If the verifier reads the algorithm from the token instead of pinning it, an attacker signs a token with HS256 using the public key as the HMAC secret. The verifier then runs HMAC with that same public key and the forged signature matches. Pinning the algorithm to RS256 stops it.

How long should a JWT live?

Keep access tokens short, on the order of a few minutes to about fifteen minutes, and issue a separate refresh token for longer sessions. A short lifetime limits how long a stolen or forged token stays useful, because a signed JWT cannot be revoked on its own once it has been issued. Always enforce the exp claim during verification.

Related articles

Press / to search · Esc