DEV Community

Cover image for The JWT alg:none Attack: Change One Header Field, Forge an Admin Token. One ESLint Rule Blocks It.
Ofri Peretz
Ofri Peretz

Posted on • Edited on • Originally published at ofriperetz.dev

The JWT alg:none Attack: Change One Header Field, Forge an Admin Token. One ESLint Rule Blocks It.

alg: "none" is a JWT header value that means "this token has no
signature — don't verify one."
It exists for unsecured tokens. It is also a
full authentication bypass the moment your verify call accepts it:

// ❌ one entry in this list is a forged-token machine
jwt.verify(token, secret, { algorithms: ["HS256", "none"] });
Enter fullscreen mode Exit fullscreen mode

The attack: change one header field

A JWT is three base64url parts: header.payload.signature. The attacker takes
any valid token and edits the header to claim no algorithm, rewrites the
payload to whatever they want, and drops the signature entirely:

# header  → {"alg":"none","typ":"JWT"}
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
# payload → {"sub":"123456","role":"admin"}
.eyJzdWIiOiIxMjM0NTYiLCJyb2xlIjoiYWRtaW4ifQ
# signature → (empty)
.
Enter fullscreen mode Exit fullscreen mode

No private key, no brute force. Because "none" is in your algorithms list,
the verifier skips signature checking, reads "role":"admin", and waves the
attacker through. CWE-347 — Improper Verification of Cryptographic Signature.

It's not theoretical

This is the 2015 disclosure (Tim McLean, "Critical vulnerabilities in JSON Web
Token libraries"
) that forced the whole ecosystem to harden. Modern
jsonwebtoken won't accept alg:none unless you explicitly allow it — which
is exactly the misconfiguration that still ships: someone adds "none" to the
algorithms array to make a test pass, or to support a legacy unsigned token,
and forgets it's there.

The fix: pin the algorithms you actually use

// ✅ only the algorithm you sign with — "none" can never match
jwt.verify(token, secret, { algorithms: ["HS256"] });
Enter fullscreen mode Exit fullscreen mode

An explicit, minimal algorithms list is the whole defense: if "none" (and
the keys you don't use) can't appear, the bypass can't happen.

The rule: no-algorithm-none (CWE-347)

You won't catch a stray "none" in a 200-file codebase by eye. The linter fails
the build on it:

npm install --save-dev eslint-plugin-jwt
Enter fullscreen mode Exit fullscreen mode
// eslint.config.mjs — `configs` is a NAMED export (default export is the plugin)
import { configs } from "eslint-plugin-jwt";

export default [configs.recommended];
Enter fullscreen mode Exit fullscreen mode
src/auth.ts
  15:3  error  🔒 CWE-347 | Using alg:"none" bypasses signature verification, allowing token forgery | CRITICAL
              Fix: Remove "none" and use RS256, ES256, or other secure algorithms
Enter fullscreen mode Exit fullscreen mode

(The ESLint CLI also appends the rule's doc URL to the Fix: line; it's trimmed
here for width.) The rule flags "none" anywhere in an algorithms array —
case-insensitively, and whether it's the only entry or buried in a list like the
one above.

The cousin: algorithm confusion

alg:none has a subtler relative. If you verify with no pinned algorithm and
hand the library your RS256 public key, an attacker re-signs a token with
that public key using HS256 — and the library, treating the public key as an
HMAC secret, accepts it:

jwt.verify(token, publicKey); // ❌ no algorithms list → RS256 verified as HS256
jwt.verify(token, publicKey, { algorithms: ["RS256"] }); // ✅ pinned
Enter fullscreen mode Exit fullscreen mode

Same root cause, same fix: always pass an explicit algorithms list.
no-algorithm-confusion and require-algorithm-whitelist enforce it. Those —
plus secret strength, claim validation (exp/iss/aud), and the
jwt.decode() trap — are the other 12 rules, walked end to end in the
eslint-plugin-jwt getting-started.


Install

# npm
npm install --save-dev eslint-plugin-jwt
# yarn
yarn add -D eslint-plugin-jwt
# pnpm
pnpm add -D eslint-plugin-jwt
# bun
bun add -d eslint-plugin-jwt
Enter fullscreen mode Exit fullscreen mode
# CI — block the PR on a re-introduced "none"
- run: npx eslint . --max-warnings 0
Enter fullscreen mode Exit fullscreen mode

Compatibility

Surface Support
Package managers npm, yarn, pnpm, bun
Node >= 18.0.0
ESLint `^8.0.0 \
JWT libraries detects {% raw %}jsonwebtoken and jose call shapes (sign/verify/decode) — reads source
Module system Plugin ships CommonJS; your config can be eslint.config.js or .mjs
Oxlint Loads under Oxlint's JS-plugin runner via the interlace-jwt port, parity-gated in CI

Links

⭐ Star on GitHub if a verify call in your codebase has ever listed "none".


I'm Ofri Peretz, a security engineering leader and the author of the
Interlace ESLint ecosystem — domain-specific static analysis for security,
reliability, and performance on the Node.js stack. eslint-plugin-jwt is its
JWT/auth layer.

ofriperetz.dev · LinkedIn · GitHub

Top comments (0)