← JWT Verification Misuse Check
decode vs verify, alg: none and how to fix eachA JSON Web Token is only worth trusting if your server proved that someone holding the key signed it. Most JWT vulnerabilities in application code are not clever cryptography attacks. They are a line that reads the token without ever checking that proof.
// reads the claims, checks nothing: anyone can write { "admin": true }
const claims = jwt.decode(token);
// proves the signature, and the expiry, with a pinned algorithm
const claims = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
jwt.decode base64-decodes the token and returns its payload without checking the signature, so anyone can write any claims. jwt.verify checks the signature (and by default the expiry) with your key and throws if the token is not genuine. Use verify for any claim you act on. Decode is fine only for claims you never trust, such as showing a name in a UI.
jwt.decode, Go ParseUnverified, Java JWT.decode, python-jose get_unverified_claims. Fix: switch to the verifying call.options={"verify_signature": False} or the older verify=False. Fix: delete the override; verification is the default."none" accepted as an algorithm, Go SigningMethodNone, Java Algorithm.none(). Fix: remove it and pin a real algorithm.jwt.verify(token, key) or jwt.decode(token, key) with no algorithms. Fix: pass the one algorithm you issue with.ignoreExpiration: true, verify_exp: False, Go WithoutClaimsValidation. Fix: delete it and issue short-lived tokens.ignoreNotBefore: true, verify_nbf: False.'secret', 'changeme' or 'shhhhh'. An HS256 secret should be at least 32 random bytes, for example from openssl rand -base64 48.req.headers, the query or the body, so the caller picks what verifies their own token. Fix: take keys from your own server-side store, looked up by kid.A JWT header names its own algorithm. If a verifier honours alg: none, a token with no signature at all passes. A related trick, algorithm confusion, hands an RS256 public key to a verifier that then treats it as an HS256 secret. Both end the same way: pin the algorithms you accept, never include none, and never let the token choose.
Paste your source into JWT Verification Misuse Check to see every finding with its file, line, risk and fix, or run the CLI in your own repository:
python3 check.py --fail-on-risk
That exits with a nonzero status if any pattern is found, so you can wire it into your own CI. Expect some findings to be deliberate (decoding a token to display a name); review each one.