Safeguard
Application Security•

ASVS 5.0 V9.1.1: Verify Token Signatures Before Trusting Them

ASVS V9.1.1 (Level 1) says self-contained tokens such as JWTs must have their signature or MAC verified before any claim is used. Code and tests.

Safeguard Security Team
3 min read

ASVS 5.0 V9.1.1 requires that self-contained tokens are validated using their digital signature or MAC before the application accepts anything inside them. It is a Level 1 requirement. In one line: never read claims from a token you have only decoded. Decode and verify are different operations, and only verify is safe.

Why it matters

A JWT is three base64url strings. Anyone can read the payload, and anyone can write a new one. Without a signature check, {"sub":"1","role":"admin"} is as good as a real token. Many real incidents come from code that called decode where it needed verify.

How to implement it

Use a maintained library, call its verify function, and handle failure as "reject the request".

import { jwtVerify, createRemoteJWKSet } from "jose";

const jwks = createRemoteJWKSet(new URL("https://idp.example.com/.well-known/jwks.json"));

export async function authenticate(token: string) {
  const { payload } = await jwtVerify(token, jwks, {
    issuer: "https://idp.example.com/",
    audience: "https://api.example.com",
    algorithms: ["RS256"],
  });
  return payload; // only now are the claims trustworthy
}
import jwt  # PyJWT

def authenticate(token, public_key):
    return jwt.decode(
        token, public_key,
        algorithms=["RS256"],
        audience="https://api.example.com",
        issuer="https://idp.example.com/",
        options={"require": ["exp", "iss", "aud"]},
    )

Rules to follow: verify before reading any claim, including for routing or logging decisions. Verify on every service that accepts the token, not only at the gateway, unless the internal network really is a trust boundary you can prove. Fail closed on any exception.

Keep the library up to date. Token libraries have had serious verification bugs, and your dependency list is part of this control. You can check yours in the browser with the free dependency scan and see how an inventory looks in the SCA demo.

How to test it

  1. Take a valid token, change one character in the payload, keep the old signature. Expect 401.
  2. Strip the signature entirely. Expect 401.
  3. Sign a token with a different key and the right header. Expect 401.
  4. Change the alg header to none. Expect 401.
  5. Grep the code for decode(, jwt_decode, verify=False, ignoreExpiration and verify_signature: False. Each hit needs a justification.

Common mistakes

  • Using a decode helper to read the sub claim.
  • Disabling verification "temporarily" in a test config that ships.
  • Catching the verification error and continuing as anonymous with the claims anyway.
  • Verifying in the gateway and trusting any internal caller that sends a token.

Try it yourself

Click through it at your own pace, or open it full screen.

Quick check

What is the difference between decoding and verifying a JWT?

Decoding only reads the base64 content. Verifying checks the signature or MAC with the right key. Only verified content may be trusted.

Which tamper test is the quickest to run?

Edit the payload and resend with the original signature. A correct implementation returns 401.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.