JWT Attacks for Pentesters: Beyond the none Algorithm
JWT attacks past alg none: algorithm confusion, weak HS256 secrets, kid injection, jwk and jku abuse, and the claim checks that actually matter.
A JWT can look solid just because it ends with a large signature. That visual confidence is often misplaced.
The token may be formatted correctly and still be accepted insecurely. The weakness usually appears in signature validation, key selection, claim handling, or excessive trust in header parameters.
The examples below assume a lab or an authorized penetration test.
Remove the magic from the token first
A common JWT has three parts separated by dots:
1
header.payload.signature
The header and payload normally use Base64URL. That is encoding, not encryption.
Decode the first two parts and the content is usually readable.
Example header:
1
2
3
4
5
{
"alg": "RS256",
"typ": "JWT",
"kid": "prod-key-01"
}
Payload:
1
2
3
4
5
6
7
8
{
"sub": "18342",
"username": "gabriel",
"role": "user",
"iss": "https://auth.lab.local",
"aud": "api.lab.local",
"exp": 1790000000
}
The signature is there to prevent silent modification of the content when the token uses JWS.
If you change "role": "user" to "role": "admin" and the server validates the signature correctly, the request should fail.
The pentest starts with that last word: correctly.
Base64URL keeps the content visible
A quick check can be done with Python:
1
2
3
4
5
6
7
8
9
import base64
import json
token = "HEADER.PAYLOAD.SIGNATURE"
payload = token.split(".")[1]
payload += "=" * (-len(payload) % 4)
data = base64.urlsafe_b64decode(payload)
print(json.dumps(json.loads(data), indent=2))
This helps identify which claims the application uses.
Look for sub, role, scope and aud. Then check iss, exp, nbf, internal IDs and tenant references.
A claim name by itself proves nothing. The useful question is which server-side decisions depend on it.
The most basic test still catches bad implementations
Some systems still accept unsigned tokens or perform validation incorrectly.
The classic case uses:
1
2
3
4
{
"alg": "none",
"typ": "JWT"
}
The signature disappears.
The token may look like this:
1
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJyb2xlIjoiYWRtaW4ifQ.
Modern libraries have reduced this problem significantly. Old implementations, custom wrappers, and unsafe configurations still deserve testing.
The goal is to learn whether the server enforces the expected algorithm or lets the token choose the rule.
The header is also user-controlled input
People often focus on the payload and ignore the header.
That is risky because parameters such as alg, kid, jwk, and jku can influence verification directly.
Consider this header:
1
2
3
4
5
{
"alg": "RS256",
"kid": "key-2026-01",
"typ": "JWT"
}
The kid helps the server identify which key to use.
That behavior is normal.
The problem starts when the backend turns that value into a file path, database query, or another key-selection mechanism without proper validation.
Algorithm confusion deserves attention
HS256 and RS256 use different trust models.
HS256 uses a symmetric secret. The same secret participates in signing and verification.
RS256 uses a key pair. The private key signs. The public key verifies.
A vulnerable implementation may accept a token that was intended for RS256 after an attacker changes the algorithm to HS256.
If the backend then uses the RSA public key as the HMAC secret, the verifier has confused two different algorithms.
Conceptually, the flow looks like this:
1
2
3
4
5
6
7
8
9
10
server expects RS256
|
v
attacker changes alg to HS256
|
v
RSA public key becomes HMAC secret
|
v
forged token passes validation
Whether this works depends on the library and on how the application chooses algorithms and keys.
A well-designed backend fixes the accepted algorithm on the server side. The token does not get to redefine that rule.
Weak HS256 secrets are useful attack material
If an application uses HS256 with a weak secret, the token can allow offline key guessing.
The server does not need to receive thousands of requests.
The attacker already has enough material inside the JWT to test candidate secrets locally.
With Hashcat:
1
hashcat -m 16500 -a 0 token.txt wordlist.txt
If the secret is recovered, arbitrary payloads can be signed with a valid key.
A secret such as:
1
secret123
remains weak even when stored in an environment variable.
The problem is key entropy.
kid can become more than an identifier
Consider a backend that loads a key like this:
1
key = open("/keys/" + header["kid"]).read()
If the value is used without validation, an input such as:
1
../../../../dev/null
may change where the verification key comes from.
Labs use this pattern because it makes the issue easy to see. In real applications, kid may also feed SQL queries or cache keys. It may also become an object name or a keystore lookup.
Follow the data.
Find where kid enters the application and how it reaches the final key.
jwk and jku change the trust boundary
The header may also embed a JWK or point to a JWKS.
Simplified example:
1
2
3
4
5
{
"alg": "RS256",
"jku": "https://keys.example.net/jwks.json",
"kid": "attacker-key"
}
If the server accepts a user-controlled origin for key retrieval, an attacker may be able to make the backend trust an attacker-owned key.
A safe implementation restricts accepted key sources and binds them to expected issuers.
Trusting any jku supplied by the token turns the trust relationship into client-controlled input.
Claims need validation too
A valid signature does not make a token valid for every context.
The token may have been issued by another system.
It may target another audience.
It may be expired.
It may not be valid yet.
It may carry a subject that should not be accepted by the current endpoint.
Common claims deserve attention.
iss identifies the issuer.
aud identifies the intended audience.
exp defines expiration.
nbf defines when the token becomes valid.
sub identifies the subject.
A correct decision combines signature, algorithm, key, and context.
An API that only asks whether the signature matches leaves a large part of the security decision unchecked.
Authorization testing is still mandatory
A JWT can be validated perfectly while the application remains vulnerable.
Example:
1
2
3
GET /api/users/18432 HTTP/1.1
Host: api.lab.local
Authorization: Bearer eyJ...
The token belongs to user 18342.
If the API returns data for user 18432 simply because the token is valid, the weakness lives in endpoint authorization.
JWT does not fix BOLA, IDOR, or broken access control.
It only carries information.
A testing sequence that saves time
I start by decoding the token.
Then I identify the algorithm and the header parameters.
Next I determine where the verification key comes from.
After that I test signature validation, algorithm handling, and claims.
Only then do I move into application authorization.
That order prevents random role edits without understanding how the server decides whether the token can be trusted.
A complete failure example
Consider an API that receives:
1
2
3
4
{
"sub": "42",
"role": "user"
}
The backend uses RS256 but also accepts HS256.
The public key is exposed through a public JWKS endpoint, which is normal for RSA verification.
An attacker obtains that public key, changes alg to HS256, changes role to admin, and signs the token using the public key as the HMAC secret.
If a vulnerable library reuses the same key variable across both algorithms, the token may be accepted.
Public-key exposure is not the weakness.
The verifier’s algorithm confusion is the weakness.
That distinction matters in a pentest report. Otherwise, a team may try to hide a key that was designed to be public while leaving the actual bug untouched.
Fixing JWT without inventing custom cryptography
The application should define accepted algorithms on the server side.
Verification keys should come from trusted sources.
kid, jwk, and jku should be treated as untrusted input.
iss and aud should be validated when they are part of the authentication model.
Tokens need expiration.
HMAC secrets need adequate entropy.
Updated libraries help, but configuration still decides whether the implementation is safe.
RFC 8725 exists because JWT provides enough flexibility to become dangerous when each application interprets its options differently.
The part I always test hardest
JWT failures usually live at the boundary between the specification and the implementation.
The token format can be correct.
The cryptography can be strong.
If the backend accepts the wrong algorithm or key, the decision breaks in application code. The same happens with a wrong issuer or claims used without proper authorization.
That is where the pentest should keep pushing.
References
IETF, RFC 7519: JSON Web Token.
IETF, RFC 8725: JSON Web Token Best Current Practices.
OWASP Cheat Sheet Series, JSON Web Token Cheat Sheet.
PortSwigger Web Security Academy, JWT attacks.


