Skip to content
Z3tra
All write-ups
3 min readPersonal labeasy

The JWT that trusted its own header

A lab write-up on an authentication bypass through JWT algorithm confusion — the classic 'alg: none' and its slightly less obvious RS256-to-HS256 cousin.

JSON Web Tokens are a good idea implemented, very often, badly. This is a short lab write-up on the two algorithm-confusion bypasses that keep appearing, because understanding why they work is worth more than memorising the payloads.

What a JWT actually is

Three base64url segments separated by dots: a header, a payload, and a signature. The header declares which algorithm signs the token. The payload holds the claims — who you are, what you can do. The signature is supposed to make the payload unforgeable.

eyJhbGciOiAiSFMyNTYifQ.eyJ1c2VyIjogImFsaWNlIn0.<signature>
   header (alg: HS256)      payload (user: alice)

The critical and frequently missed point: the token tells the server how to verify itself. That is the whole vulnerability class in one sentence.

Bypass one: alg none

The JWT specification includes a none algorithm — a token with no signature at all, intended for cases where integrity is guaranteed by some other layer. A verifier that honours the header without checking it against what it expects will accept a none token as validly signed, because an empty signature is exactly what none requires.

Decode the token

Split on the dots, base64url-decode the first two segments. Now you can read the header and the claims.

Rewrite the header and the claim

Set the algorithm to none, and change the identity claim to whatever you want to be.

Drop the signature

A none token ends with a trailing dot and nothing after it. If the server accepts it, it never actually verified anything.

import base64, json
 
def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
 
header = {"alg": "none", "typ": "JWT"}
claims = {"user": "admin", "role": "admin"}
 
forged = (
    b64url(json.dumps(header).encode())
    + "."
    + b64url(json.dumps(claims).encode())
    + "."   # no signature — that is the point
)

The lab accepted it. In the real world a correct library rejects none by default now, but "correct library, default config, kept updated" is three assumptions and any one of them can be false.

Bypass two: RS256 to HS256

This one is subtler and more interesting. RS256 signs with a private key and verifies with a public one. HS256 signs and verifies with the same secret.

The public key is, by definition, public. So if a server verifies whatever algorithm the header names, an attacker can:

  1. Take the server's public RSA key — it is not a secret.
  2. Forge a token with the header set to HS256.
  3. Sign it with HMAC, using the public key bytes as the HMAC secret.

The server, told the token is HS256, verifies it with HS256 — using the public key as the secret, because that is the key it has. The signature checks out. The server has just used a value the attacker also possesses to verify a token the attacker forged.

The fix

Do not let the header pick the algorithm. Pin it.

// Fragile: verify() trusts token.header.alg
jwt.verify(token, key);
 
// Sturdier: the server dictates what is acceptable, the token does not.
jwt.verify(token, key, { algorithms: ["RS256"] });

One option. It closes both bypasses at once, because both depend on the server being willing to verify an algorithm it did not choose. Take that choice away from the attacker and the entire class disappears.

Related

Write-up4 min

From a blind SQL injection to a shell, one bit at a time

Personal lab

A lab walkthrough: finding a boolean-blind injection with no visible output, turning it into data exfiltration, then into file write, then into code execution.

  • Write-ups
  • Web Security
  • SQL Injection
Article2 min

The JWT mistakes that still work in 2026

Algorithm confusion, unverified signatures and secrets that were never secret. A tour of the JWT failures I keep finding in labs, and what each one actually requires to exploit.

  • Web Security
  • JWT
  • Authentication
Research4 min

Encrypting content is easy. Hiding who talks to whom is not.

Notes from designing Orbyte: why metadata is often more sensitive than message content, and the spectrum of defences between 'we encrypt messages' and actual metadata resistance.

  • Research
  • Web Security
  • Cryptography