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.
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
secretorpassword→ guessable
Then an attacker can create their own valid tokens for any user or role.
4.3 Brute-Forcing the Secret Key
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:
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.
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:
- Generate a new RSA key pair in JWT Editor
- Change the payload (
"role": "admin") - Click "Attack" → "Embedded JWK"
- The extension inserts your public key as
jwkand signs with your private key - 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:
- Host your own JWKS on an attacker-controlled server
- Change
jkuin the header to your URL - The server fetches your JWKS and verifies with your public key
- 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:
- 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
| 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:
- Find the public key via
/jwks.jsonor extract it viax5t - Try algorithm confusion: change RS256 → HS256
- Sign the token with the public key as the HMAC secret
- Check whether
kidis vulnerable to directory traversal