Skip to content

JWT Webtokens

Source: PortSwigger Web Security Academy Author: Johan Beysen


1. What Is a JWT Token?

A JWT (JSON Web Token) is a digital passport that the server creates when you log in. It contains who you are and what you're allowed to do, and is cryptographically signed against tampering.

Classic Session vs JWT

Classic session JWT
Server remembers Yes — session state stored No — stateless
Client receives Only a session code The full token with data
Verification Server queries the session store Server verifies the signature
Scalability All servers must know the session Every server can validate independently

Use case

Say you have 5 different servers. With a classic session, every server needs to know who you are. With JWT you just send your token along — every server independently verifies the signature.


2. Structure of a JWT

A JWT consists of three parts, separated by a dot: header.payload.signature

Part Description
Header Which algorithm is used for the signature
Payload Your data: name, role, permissions, ...
Signature The cryptographic signature protecting everything

The header and payload are just base64url-encoded JSON — readable via jwt.io or Burp Decoder. That's why the cryptographic signature is crucial.

JWS vs JWE

Variant Payload Description
JWT — The concept/envelope
JWS (JSON Web Signature) Readable (base64) Signed so no one can modify it
JWE (JSON Web Encryption) Fully encrypted No one can read it without the key

JWS is what everyone means when they say "JWT." Structure: header.payload.signature


3. JWT Signature

The server takes the header + payload, runs it through a hash function together with a secret key, and the result is the signature.

Change one byte = signature invalid = token rejected

Vulnerability

In principle, you get nowhere without the secret key. You can read the header and payload, but you can't create new valid tokens.


4. JWT Attacks

4.1 Problem 1 — The Signature Isn't Checked

The developer uses decode() instead of verify():

Function Behavior
verify() Decodes and checks the signature
decode() Decodes only, no check

If decode() is used, the server accepts modified tokens without any signature check.

Attack: Modify the payload (e.g. "role": "admin") and send the token back. The server just accepts it.

4.2 Problem 2 — a Weak or Leaked Secret Key

If the secret key:

  • Is too weak → brute-forceable
  • Accidentally ends up on GitHub → leaked
  • Is a default value like secret or password → guessable

Then an attacker can create their own valid tokens for any user or role.

4.3 Brute-Forcing the Secret Key

hashcat -a 0 -m 16500 <jwt> <wordlist>

Building a wordlist

Combine every known password (from the dev team or the company) into a wordlist. Also use standard JWT wordlists available on GitHub.


5. Algorithm Confusion — alg: none

The header of a JWT contains the algorithm:

{ "alg": "HS256", "typ": "JWT" }

Change this to "alg": "none" — this means: no signature needed.

Some servers reject this, but you can bypass the filter with variants like: - None - NONE - nOnE

If the server doesn't filter properly, it accepts a token with no signature at all.

# A token with alg: none has no signature
header.payload.   ← empty signature

6. Abusing Header Parameters

Besides alg, JWT headers can also contain:

Parameter Description
jwk An embedded JSON object with the key
jku The URL the server fetches keys from
kid The ID of the key the server should use

These tell the server which key it should use to verify the signature.

6.1 Self-Signed JWT via the jwk Parameter

Ideally the server only uses a whitelist of trusted public keys. With a bad configuration, the server uses whatever key is embedded in the jwk parameter.

Attack with the Burp JWT Editor extension:

  1. Generate a new RSA key pair in JWT Editor
  2. Change the payload ("role": "admin")
  3. Click "Attack" → "Embedded JWK"
  4. The extension inserts your public key as jwk and signs with your private key
  5. The server verifies with the embedded key → attack succeeds

6.2 Self-Signed JWT via the jku Parameter

JWK Sets are sometimes publicly available via /.well-known/jwks.json.

Attack:

  1. Host your own JWKS on an attacker-controlled server
  2. Change jku in the header to your URL
  3. The server fetches your JWKS and verifies with your public key
  4. You signed with the matching private key → attack succeeds

Whitelist bypass

More secure servers only fetch keys from trusted domains. Try URL parsing discrepancies like SSRF bypass techniques.

6.3 Self-Signed JWT via the kid Parameter

kid = the ID of the key, stored on the server. The value comes from the token itself — so from the client.

Attack — Path Traversal via kid:

{ "kid": "../../dev/null" }
  • The server looks up that "file" → /dev/null = an empty file = an empty string
  • You sign the token with an empty string as the secret
  • The server verifies with that same empty string → match!
  • You now have a self-made token the server fully trusts

7. Algorithm Confusion Attack (RS256 → HS256)

With asymmetric algorithms (RS256):

  • The server signs with the private key
  • The server verifies with the public key

Attack: Change the algorithm from RS256 to HS256. The server now thinks HS256 is being used and verifies with its public key as the HMAC secret. You also sign the token with that same public key.

Extracting the Public Key

If the public key isn't known:

# Via a Docker container
docker run --rm -it portswigger/sig2n <token1> <token2>

# Via a Python tool
python3 jwt_forgery.py <token1> <token2>

Attack Plan for an RS256 Token

{
  "alg": "RS256",
  "kid": "F0EBED7A...",
  "x5t": "8OvteoSv...",
  "typ": "at+jwt"
}
Parameter Analysis
alg: RS256 Asymmetric → vulnerable to algorithm confusion
kid ends in RS256 Test whether a key with ...HS256 exists
x5t X.509 certificate thumbprint → public key is in the certificate
typ: at+jwt OAuth2 Access Token → Bearer token in the Authorization header

Steps:

  1. Find the public key via /jwks.json or extract it via x5t
  2. Try algorithm confusion: change RS256 → HS256
  3. Sign the token with the public key as the HMAC secret
  4. Check whether kid is vulnerable to directory traversal