JWT Misconfiguration

ID

jwt_misconfiguration

Severity

critical (alg=none / verify disabled) / high (hardcoded secret / exp disabled)

Remediation Complexity

trivial

Remediation Risk

high

Remediation Effort

low

Family

API2:2023 - Broken Authentication

CWE

CWE-347, CWE-327, CWE-757

Resource

authentication

Language

Java, JS/TS, Python, C#, Go, PHP

Description

Scans handler files for common JWT (JSON Web Token) security misconfigurations across Java (JJWT, com.auth0.jwt), JS/TS (jsonwebtoken, jose), Python (PyJWT, python-jose), C# (Microsoft.IdentityModel.Tokens, System.IdentityModel.Tokens.Jwt), Go (golang-jwt), and PHP (firebase/php-jwt).

Severity is tiered by signal:

  • CRITICAL — algorithm none accepted (trivially forgeable tokens), or signature verification disabled.

  • HIGH — hardcoded HMAC secret in source, or expiration validation disabled.

Rationale

JWT is the dominant API authentication token format, and almost every JWT failure mode collapses to the same outcome: an attacker mints a token that the server accepts as if it had been issued by the auth service. Concrete attacks the misconfigurations enable:

  • alg: none accepted — the attacker sends a token with the header {"alg":"none"} and an arbitrary payload. The library reads the header, sees no signature is required, and trusts the payload. This was the 2015 JJWT / node-jsonwebtoken flaw, still re-introduced regularly in hand-rolled validators.

  • Signature verification disabledjwt.decode() instead of jwt.verify(), verifySignature=false, the legacy parseClaimsJwt() API. Same outcome: any payload is trusted.

  • Hardcoded HMAC secret — once the secret is in source, it is in the repo history, in dependency-mining bots' caches, and in every developer’s laptop. Anyone with read access to the repo can forge tokens. HMAC’s symmetry means there is no separate verify key to limit blast radius.

  • Expiration disabled — a token that never expires is a long-lived credential. Combined with one leak (browser extension, request log, MITM on a developer device, …) it becomes a permanent backdoor.

Asymmetric algorithms (RS256 / ES256) with a server-side private key avoid both the alg confusion class of attacks and the symmetric-secret problem entirely.

Remediation

  • Pin the algorithm. Validate against an explicit allowlist: Jwts.parserBuilder().setSigningKey(key).build() (JJWT 0.11+), jwt.verify(token, key, {algorithms: ['RS256']}) (jsonwebtoken), jwt.decode(token, key, algorithms=['RS256']) (PyJWT). Never accept 'none'.

  • Always verify. Use the verify / validate API, not the raw decode.

  • Use asymmetric keys (RS256, ES256, EdDSA). The auth service signs with a private key; resource servers verify with the public key. Compromising one resource server does not let an attacker mint tokens.

  • Validate the standard claims. exp (expiration), nbf (not-before), iss (issuer), aud (audience) — most libraries check these only if you ask.

  • Never store secrets in source. Use the platform secret store (Kubernetes secret, AWS Secrets Manager, HashiCorp Vault, GitHub Actions secret) and load at runtime.

  • Don’t store sensitive data in the payload. JWT bodies are base64, not encrypted. Email addresses, role labels, and an opaque user id are typical; anything beyond that should be referenced by id and re-fetched server-side.

  • Implement revocation — short-lived access tokens + refresh-token rotation, or a denylist keyed by jti. JWT itself does not have a built-in revocation mechanism.

Configuration

The detector recognises the dangerous JWT idioms via per-language regex over the handler file. The set of dangerous algorithms (none, HS256 with weak secrets) and the signature-bypass APIs are built in.

Unless your project wraps the standard JWT libraries behind a custom name, you typically do not need to configure this detector.

References