JSON Web Tokens feel secure. They are signed, they are opaque-looking base64, and the whole point is that a tampered token fails verification. That confidence is exactly what makes them dangerous, because the most common JWT breaches are not brute-forced signatures — they are servers that trust the token a little too much. Two classic attacks let an attacker forge a valid-looking token outright, and both are documented in RFC 8725, the IETF's JWT Best Current Practices. The good news is that a single principle defeats both.

First, a JWT is signed — not encrypted

Before the attacks, clear up the most common misconception. A JWT has three parts separated by dots: a header, a payload, and a signature, each base64url-encoded. Encoding is not encryption. Anyone who holds the token can decode the payload and read every claim in it — no key required. Paste a token into any decoder and its contents are right there in plaintext.

The signature does not hide the payload; it only proves the payload has not been altered by someone without the key. The practical rule: never put anything sensitive in a JWT payload — no passwords, no secrets, no PII you wouldn't want a client to see. The token is a tamper-evident envelope, not a sealed one.

Attack 1: the 'alg: none' trap

Every JWT header declares which algorithm signed it, e.g. "alg": "HS256". The JWT spec also defines an "unsecured" algorithm value: none, meaning the token has no signature at all. You can see where this goes. As RFC 8725 Section 2.1 puts it, "the algorithm can be changed to 'none' by an attacker, and some libraries would trust this value and 'validate' the JWT without checking any signature."

The exploit is trivial: take a legitimate token, edit the payload to make yourself an admin, set the header's alg to none, drop the signature entirely, and send it. A naive library sees alg: none, concludes there is nothing to verify, and accepts the forged token. RFC 8725 Section 3.2 is explicit about the fix: "JWT libraries SHOULD NOT consume JWTs using 'none' unless explicitly requested by the caller."

Attack 2: algorithm confusion (RS256 → HS256)

The second attack is subtler and hits systems that think they did the right thing by using asymmetric crypto. With RS256, the server signs tokens with a private RSA key and verifies them with the corresponding public key — which, being public, is often freely available. HS256 is different: it is a symmetric HMAC where the same secret both signs and verifies.

The confusion attack exploits a server that will accept either. As RFC 8725 Section 2.1 describes it: an "'RS256' ... parameter value can be changed into 'HS256' ... and some libraries would try to validate the signature using HMAC-SHA256 and using the RSA public key as the HMAC shared secret." So the attacker flips the header to HS256, then signs their forged token with HMAC using the server's public RSA key as the secret. When the server verifies, it also reaches for that public key — and because the attacker used the very same value as their HMAC secret, the signature matches. A public, non-secret key just became the signing key. Forged token accepted.

The fix: pin your algorithms, don't trust the header

Both attacks share one root cause: the server let the token decide which algorithm to use for verification. The fix, from RFC 8725 Section 3.1 ("Perform Algorithm Verification"), is unambiguous: "Libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms ... The library MUST ensure that the 'alg' ... header specifies the same algorithm that is used for the cryptographic operation."

In practice: decide server-side which algorithm(s) you accept, and pass an explicit allowlist to your verify call. Never let the incoming token's alg header pick the verification path.

python
# Illustrative (PyJWT) — the allowlist is the whole defense
import jwt

# ✅ Pin the algorithm. A token claiming HS256 or none is rejected outright.
payload = jwt.decode(token, public_key, algorithms=["RS256"])

# ❌ Never accept whatever the token's header declares.
#    (Omitting or widening `algorithms` is how both attacks get in.)

Two more essentials. If you do use HS256, the secret must be a high-entropy value from a cryptographically secure generator — at least 256 bits (32 bytes), kept in a secrets manager, never a human-memorable password (weak secrets are separately brute-forceable). And because JWTs are stateless and cannot be individually revoked, keep access-token lifetimes short and pair them with a refresh-token mechanism, so a leaked token has a small window of usefulness.

Key Takeaways

  • A JWT is signed, not encrypted — the payload is readable by anyone; never store secrets in it.
  • alg: none lets attackers strip the signature; libraries must not consume none tokens by default.
  • Algorithm confusion (RS256→HS256) lets attackers sign forged tokens with your public key as the HMAC secret.
  • Pin algorithms server-side with an explicit allowlist and never trust the token's alg header — this defeats both attacks.
  • Use strong random HS256 secrets (≥256 bits), and keep token lifetimes short since JWTs can't be revoked individually.

References