← All posts

How I Test for JWT Validation Flaws

How I Test for JWT Validation Flaws

Most APIs that use JWTs check whether a token is present and whether it can be decoded. They stop there. The problem is that decoding a JWT requires no secret and no verification. Any string that looks like three base64 segments separated by periods will decode successfully. The application extracts the user ID from the payload, loads that user's data, and grants access. An attacker who changes the sub claim from 1234 to 5678 becomes user 5678.

This happens because developers treat JWTs as opaque tokens when they are actually signed or encrypted data structures. The signature exists to prevent tampering, but only if the application actually verifies it. I see APIs that decode the payload and immediately trust every claim inside without ever checking the signature against a secret or public key.

Why This Hides

JWT libraries make verification optional. Most provide separate methods for decoding and verifying, and the decode method works perfectly during development. The token parses, the claims look correct, and everything functions. The missing verification step causes no errors and no visible breakage. Code review often misses it because the JWT handling is tucked into middleware or a utility function, and reviewers assume the library is doing the secure thing by default. It is not.

My Testing Method

  1. Capture a valid JWT from an authenticated session. Decode it using a tool or library to see the header and payload structure. Note the algorithm in the alg header and identify which claims control identity, such as sub, user_id, or email.
  2. Modify a claim that determines access or identity. Change the user ID to another valid value, or escalate a role from user to admin. Re-encode the header and payload without signing, or sign it with a key you control.
  3. Send the modified token in a request to a protected endpoint. If the API accepts it and returns data for the altered user or role, signature verification is missing or broken.
  4. Test the alg field by changing it to none. Remove the signature portion entirely, leaving the trailing period. Some libraries honor this and skip verification when alg is set to none.
  5. If the API uses asymmetric signing with RS256, try changing the algorithm to HS256 and signing the token with the public key. If the server uses the same key material to verify HMAC signatures that it uses for RSA public key verification, this can succeed.
  6. Check for missing expiration enforcement by replaying an old token or by removing the exp claim entirely. Verify that the API rejects expired tokens and enforces reasonable TTLs.
  7. Test issuer and audience validation by changing the iss or aud claims to arbitrary values. These claims tie a token to a specific system and should be validated, but often are not.

The Deeper Issue

A valid JWT structure is not the same as a verified JWT. Parsing succeeds even when the signature is wrong, missing, or never checked. Treat every JWT as untrusted input until signature verification passes. Do not extract claims before verification, because an attacker controls the entire token body until you prove it was signed by your system.

Defensive Implementation

When implementing JWT validation, always use the verify method provided by your library, never the decode method alone. Explicitly specify the allowed algorithms and reject none. Validate iss, aud, and exp claims as part of every verification call. Store signing secrets securely and rotate them periodically.

const jwt = require('jsonwebtoken');

function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],
      issuer: 'api.example.com',
      audience: 'web-client'
    });
    return decoded;
  } catch (err) {
    throw new Error('Invalid token');
  }
}

Why It Persists

JWT validation flaws survive because the failure mode is silent. An application that skips verification works exactly like one that performs it, until an attacker notices. Developers see tokens flowing through the system and assume the library is handling security. Documentation often shows decoding examples first and buries verification in later sections. The gap does not surface in functional testing, only in adversarial testing.

I test every API that uses JWTs by modifying tokens and observing whether the changes are rejected. Verification must happen before trust.