HackproofHacks
API Security 17 min read

JWT Security: Common Vulnerabilities and How to Test Them

A hands-on JWT security guide: how JSON Web Tokens work, the vulnerabilities that plague them, and three worked tests with jwt_tool and hashcat, from alg:none forgery to algorithm confusion and cracking weak secrets.

Hassan Ansari

Hassan Ansari

· Updated Jul 8, 2026
A code card showing a JWT header with alg set to none, a classic forgery

JWT security: vulnerabilities and how to test them

JSON Web Tokens are everywhere in modern applications. They power stateless authentication across APIs, single-page apps, and microservices, letting a server verify who a user is without storing session state. That convenience is exactly why JWT bugs are so valuable: when the token is the proof of identity, a flaw in how it is validated can mean full authentication bypass. Change one field, become the admin.

This guide explains how JWTs work, the vulnerabilities that show up again and again in real applications, and, most importantly, how to test for them with actual commands. It pairs naturally with the broader API security testing guide.

Practise legally: every test below assumes a token from a system you own or are explicitly authorised to test. The best free place to build these skills is PortSwigger’s Web Security Academy, which has a full set of JWT labs you are allowed to break. On a real engagement, stay strictly inside your authorised scope. Forging a token against a login you have no permission for is unauthorised access, plain and simple.

How a JWT works

The format is standardised in RFC 7519. A JWT is three Base64URL-encoded parts joined by dots:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoxMDQyLCJyb2xlIjoidXNlciJ9.KmA3...signature
     header               payload                          signature
  • Header: JSON describing the token, most importantly the signing algorithm (alg), for example {"alg":"HS256"}.
  • Payload: JSON containing the claims: who the user is, their role, when the token expires, for example {"user":1042,"role":"user","exp":1720000000}.
  • Signature: a cryptographic signature over the header and payload, using either a shared secret (HMAC, like HS256) or a private key (asymmetric, like RS256).

Here is the point most beginners miss: the header and payload are only encoded, not encrypted. Anyone with the token can decode and read them. Paste one into jwt.io and every claim is immediately legible. The signature does not hide the contents, it only proves they have not been changed. The server trusts a JWT because it can recompute and verify the signature. Break or bypass that verification and the whole trust model collapses.

The two root causes of almost every JWT bug

Nearly every JWT flaw comes down to one of two failures: the signature is not verified properly, or the claims are not validated properly. Keep those two buckets in mind and the specific bugs below stop feeling like a random list and start feeling like variations on a theme.

Signature failures

  • alg: none. The spec includes a none algorithm, meaning no signature at all. If a server accepts {"alg":"none"} and skips verification, an attacker rewrites the payload (say, role from user to admin), drops the signature, and sends it. A vulnerable server trusts it.
  • Weak HMAC secret. Tokens signed with HS256 depend entirely on a shared secret. If that secret is a dictionary word or a short default, anyone who captures one valid token can brute-force it offline and then sign any token they want.
  • Algorithm confusion (RS256 to HS256). In RS256, tokens are signed with a private key and verified with a public key that is not secret. If the verifier lets the token’s header pick the algorithm, an attacker switches alg to HS256 and signs with the public key as the HMAC secret. A naive server validates the forgery because it uses that same public key as the HMAC key.
  • Signature simply not checked. Sometimes a developer decodes the payload to read claims but forgets to verify the signature first. The token becomes attacker-editable JSON.

Claim failures

  • No expiry check. A token with no exp, or a server that ignores it, means a stolen token never stops working.
  • Missing audience or issuer checks. If the server does not validate aud and iss, a token issued for one service or environment may be accepted by another that shares a key.
  • Sensitive data in the payload. Because the payload is readable by anyone, putting secrets, API keys, or personal data in claims is a straight disclosure bug. Treat the payload as public.
  • kid header injection. The optional kid (key ID) header tells the server which key to use. If the server uses that attacker-controlled value unsafely, to build a file path or run a query, it can become a vector for path traversal or SQL injection.

How to test JWT security: three worked attacks

Reading about these is one thing. Running them is what makes the flaw click. First, find the token. JWTs usually ride in the Authorization: Bearer ... header, but they also hide in cookies, in localStorage, and in request bodies, so watch the traffic in Burp Suite while you log in and note every place a dotted eyJ... string appears. Any of them is a candidate to test.

The go-to tool is jwt_tool (ticarpi/jwt_tool), which decodes, tampers, forges, and cracks tokens. Install it, grab a valid token from your authorised target, and start by just decoding it:

python3 jwt_tool.py eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoxMDQyLCJyb2xlIjoidXNlciJ9.KmA3...
Token header values:
[+] alg = "HS256"
[+] typ = "JWT"
Token payload values:
[+] user = 1042
[+] role = "user"
[+] exp = 1720000000

Now you know the algorithm and the claims. That tells you which of the three attacks to try.

Attack 1: forge an unsigned token (alg:none)

The fastest thing to check is whether the server accepts an unsigned token. jwt_tool’s exploit mode builds the none variants for you:

python3 jwt_tool.py <TOKEN> -X a
[+] alg:none forged token (variant "none"):
eyJhbGciOiJub25lIn0.eyJ1c2VyIjoxMDQyLCJyb2xlIjoidXNlciJ9.

To make yourself an admin, run tamper mode (-T) first, change role to admin, then re-issue with the none algorithm. Send the forged token in place of the real one and watch the response. If the server treats you as an admin, the signature is not being verified. That is a critical finding.

Attack 2: crack a weak HMAC secret

If the token uses HS256, its whole security rests on the secret. Put the full token in a file and let hashcat run a wordlist against it in JWT mode:

echo '<TOKEN>' > jwt.txt
hashcat -m 16500 -a 0 jwt.txt /usr/share/wordlists/rockyou.txt

-m 16500 is hashcat’s JWT (HMAC) mode. If the secret is weak, it falls out in seconds:

<TOKEN>:secret123

Now you own the signing key. You can mint a token with any claims you like and it will validate perfectly, because it is signed with the real secret:

python3 jwt_tool.py <TOKEN> -T -S hs256 -p "secret123"

This is why HMAC secrets must be long, random, and unguessable. rockyou.txt should never contain your signing key.

Attack 3: algorithm confusion (RS256 to HS256)

This is the subtle one, and the highest-impact when it works. If the app signs with RS256, the public key is meant to be shared, and it is often published at a JWKS endpoint. Grab it:

curl -s https://target.com/.well-known/jwks.json -o jwks.json

Convert that JWK to a PEM public key (jwt_tool ships helpers for this, or use any JWK-to-PEM converter), then run the key-confusion exploit, which re-signs the token as HS256 using that public key as the HMAC secret:

python3 jwt_tool.py <TOKEN> -X k -pk public.pem

If the server was written to “verify with the public key” without pinning the algorithm, it validates the forged HS256 token, because it uses the same public key as the HMAC key. From there you forge any user, including administrators.

Attack 4: abuse the header (kid and jku)

Two optional header fields are worth probing, because they tell the server where to find its key. The kid (key ID) header points at which key to load. If the server feeds that value into a file path or a database query without sanitising it, you can redirect verification at something you control: a path-traversal kid can point at a predictable file whose contents you already know (so you sign with that value), and a SQL-injectable kid can be coerced into returning a key you chose.

The jku (JWKS URL) header is even more direct. It tells the server where to fetch the public keys. If the server trusts an attacker-supplied jku, you host your own key set at a URL you control, point the token at it, and sign with your matching private key. jwt_tool automates both the inline-JWKS injection and the spoofed-URL attack:

python3 jwt_tool.py <TOKEN> -X i   # inject an inline JWKS into the token header
python3 jwt_tool.py <TOKEN> -X s   # spoof a JWKS hosted at a URL you control

The rule of thumb: any header field that decides which key verifies the token is a field worth attacking.

Also check the boring stuff

Between those three, work the checklist: does an expired token still work? Does a token with a wrong aud or iss get accepted? Does changing a claim without touching the signature get through (proving the signature is not checked)? And if a kid header is present, test it for injection. The exotic attacks get the attention, but a missing expiry check is a real finding too.

Where to practise

Do not learn any of this for the first time on a live target. PortSwigger’s Web Security Academy has a dedicated set of free JWT labs covering exactly these attacks: unverified signatures, alg:none, weak secrets, and algorithm confusion. Every lab is legal to break, and working through them with jwt_tool is the fastest way to build the instinct for spotting the non-obvious cases.

How to secure JWTs

Robust JWT security is mostly strict configuration and using a vetted library correctly. The single most important rule ties every attack above together: pin the algorithm and verify before you trust. Decide the exact algorithm your app uses, reject everything else including none, and never let the token’s own header choose how it is verified.

In practice, that means configuring your library explicitly rather than accepting its defaults. Here is what a strict verification looks like in Python with PyJWT:

import jwt

def verify(token: str) -> dict:
    return jwt.decode(
        token,
        key=PUBLIC_KEY,
        algorithms=["RS256"],              # reject none, HS256, and everything else
        audience="my-api",                 # validate aud
        issuer="https://auth.example",     # validate iss
        options={"require": ["exp", "iat"]},  # force expiry to be present and checked
    )

Around that core, apply the rest:

  • Use strong keys and secrets: long, random HMAC secrets, and properly managed private keys for asymmetric signing.
  • Validate all claims, not just the signature: exp, nbf, aud, and iss on every request.
  • Keep tokens short-lived: pair short access-token lifetimes with refresh tokens and a revocation strategy, since a plain JWT cannot be invalidated early.
  • Never store secrets in the payload. Treat it as public.
  • Protect the transport: always send tokens over HTTPS so they cannot be intercepted. Verify your TLS setup with the SSL/TLS Checker.

One connection worth noting: because JWTs are often sent in an Authorization header rather than a cookie, cookie-based attacks like CSRF usually do not apply to them. But that only holds if you do not store the token in a cookie. Your storage choice changes the threat model.

Key takeaways

  • A JWT’s header and payload are encoded, not encrypted, and readable by anyone. The signature only proves integrity.
  • Almost every JWT bug is a signature failure (alg:none, weak secret, algorithm confusion, skipped verification) or a claim failure (missing expiry, audience, or issuer checks).
  • Test with jwt_tool and hashcat: forge an alg:none token, crack a weak HS256 secret, and try RS256-to-HS256 confusion against the published public key.
  • Secure JWTs by pinning the algorithm and verifying before trusting, using strong keys, validating every claim, and keeping tokens short-lived.

When the token is the identity, its verification logic is the lock on your entire application. Get it right and JWTs are a clean way to do stateless auth. Get it wrong and it is a single header change away from admin.

For implementation guidance, the OWASP JSON Web Token Cheat Sheet covers every validation step your library should be doing, and if your tokens are issued through OAuth 2.0, RFC 6749 is the spec that governs the flow. For cracking weak HMAC secrets, Hashcat supports JWTs natively.

Continue with the API security testing guide and the IDOR and access control explainer. Build these skills hands-on with our training, and if you want your own API’s token handling reviewed properly, that is what our security assessments are for.

#JWT #JSON Web Token #API security #authentication #OWASP #tokens
Free newsletter

Liked this? I write one like it every week.

One practical security lesson in your inbox each week, explained the same simple way. Join 10,000+ readers. Unsubscribe anytime.

From the article

Need a security assessment?

HackproofHacks provides web application and API penetration testing — using the same techniques covered in this article, with your explicit authorisation.

Book a free scoping call

More on API Security.

All articles →
FAQ

Questions about this topic.

What is a JWT (JSON Web Token)?

A JSON Web Token is a compact, self-contained token used to carry claims, usually about who a user is, between a client and a server. It has three parts separated by dots: a header describing the signing algorithm, a payload containing the claims like user ID and expiry, and a signature. The first two parts are just Base64URL-encoded JSON, so anyone can read them; the signature is what proves the token has not been tampered with. JWTs are popular for stateless authentication because the server can verify the signature and trust the claims without storing session state.

Is the data inside a JWT encrypted?

No, and this is a critical misconception. A standard signed JWT is encoded, not encrypted. The header and payload are Base64URL-encoded JSON that anyone who has the token can trivially decode and read. The signature prevents modification, not reading. This means you must never put sensitive data like passwords, secrets, or personal information you do not want exposed in a JWT payload, because anyone who intercepts or is issued the token can read every claim inside it.

What is the JWT alg:none vulnerability?

The JWT standard includes a none algorithm, meaning the token is unsigned. If a server accepts a token with the header algorithm set to none and skips signature verification, an attacker can craft any payload they like, for example changing the user ID or role to admin, remove the signature, and the server will trust it. It is one of the oldest and most dangerous JWT flaws. The fix is to explicitly reject none and only accept the specific algorithm your application expects.

What is a JWT algorithm confusion attack?

Algorithm confusion, also called key confusion, happens when a server that is meant to verify tokens with an asymmetric algorithm like RS256 can be tricked into verifying them with a symmetric algorithm like HS256. In RS256 the server verifies using a public key, which is not secret. If an attacker changes the token's algorithm to HS256 and signs it using that public key as the HMAC secret, a naive verification routine may validate it, because it uses the same public key as the HMAC key. The defence is to pin the expected algorithm and never let the token's own header decide how it is verified.

How do you securely implement JWT authentication?

Use a well-maintained library and configure it strictly: pin the exact signing algorithm your app expects and reject anything else including none, use a strong secret for HMAC or properly managed keys for asymmetric signing, and always verify the signature before trusting any claim. Validate the standard claims: expiry (exp), not-before (nbf), audience (aud), and issuer (iss). Keep token lifetimes short and pair them with refresh tokens and a revocation strategy, since a plain JWT cannot easily be invalidated before it expires. Never store sensitive data in the payload.

Can a JWT be revoked or logged out?

Not easily, and that is one of the trade-offs of stateless tokens. Because a standard JWT is validated purely by its signature and expiry, the server has no built-in way to invalidate one before it expires, so a stolen token remains valid until it times out. Teams address this by keeping token lifetimes short, using refresh tokens that can be revoked, and maintaining a server-side denylist of revoked token identifiers for sensitive applications. If instant revocation matters to your app, you must build one of these mechanisms deliberately.