Mesrai
Back to blog
// essaySecurity

JWT alg: "none" Vulnerability: How Mesrai Spots It

Real PR catch: `jwt.verify` without algorithms allowlist accepts `alg:none` forged tokens. Full attack + pinned-algorithm fix. CWE-347.

Mesrai TeamJuly 22, 20268 min read

An auth middleware verifying JWTs. The line looked correct — uses the official `jsonwebtoken` library, calls `verify`, passes the secret. The bug was what wasn't there: the algorithms option. Without it, the library accepts whatever algorithm the token header claims — including `none`.

The vulnerable diff

tsauth/middleware.ts
// auth/middleware.ts
import jwt from "jsonwebtoken";

export function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  // BUG: no algorithms option — library accepts alg:none
  const decoded = jwt.verify(token, SECRET);
  req.user = decoded;
  next();
}

What is wrong

The `jsonwebtoken` library historically defaulted to honoring whatever algorithm the JWT header specified. Without an explicit `algorithms` allowlist, an attacker crafts a token with `{"alg":"none","typ":"JWT"}` and an empty signature; the library treats the token as valid because `alg:none` requires no signature. Newer versions of the library warn about this, but the bug class is still common in older codebases and even in some 2026 PRs. CWE-347 covers this and the related `alg` confusion attacks (HS256/RS256 switch).

The attack

Forging a token takes one line of code:

tsexploit
// Attacker forges an admin JWT — no signature needed
const header  = Buffer.from(JSON.stringify({alg:"none", typ:"JWT"})).toString("base64url");
const payload = Buffer.from(JSON.stringify({sub:"admin", role:"admin", exp: Date.now()/1000 + 3600})).toString("base64url");
const forged  = `${header}.${payload}.`;  // empty signature

await fetch("/api/admin/users", { headers: { authorization: `Bearer ${forged}` }});
// 200 OK — server-side jwt.verify accepted it.

From there the attacker is admin. Variants include `alg:HS256` with the RS256 public key as the HMAC secret — a different but related attack class.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 90 sec ago

[mesrai] [code-review] [Security] [CWE-347] [critical]

`jwt.verify(token, SECRET)` without an `algorithms` option lets the
library accept `alg:none` tokens. Attacker forges a token with header
`{"alg":"none"}` and empty signature — server accepts it as valid.

Always pin the algorithm:

  const decoded = jwt.verify(token, SECRET, {
    algorithms: ["HS256"],
    issuer: "app.example",
    audience: "api.example",
  });

Also pin issuer and audience to prevent token reuse across services.
For asymmetric (RS256/ES256) keys, pass the public key — never accept
a mix of HS and RS in the same allowlist.

Reference: OWASP JWT Cheat Sheet, CWE-347

The fix

tsauth/middleware.ts (fixed)
// auth/middleware.ts — fixed
const decoded = jwt.verify(token, SECRET, {
  algorithms: ["HS256"],
  issuer: "app.example",
  audience: "api.example",
});

Three options: pin the algorithm to a single value matching how you sign tokens, pin the issuer so tokens from other services do not validate here, pin the audience so this token cannot be replayed against a sister service. All three are one-line additions.

Why human review missed it

JWT verification looks safe because it uses a vetted library. Reviewers trust the library's defaults. The defaults changed across `jsonwebtoken` major versions, so the same code is safe on one version and unsafe on the previous one. Mesrai's rule pack catches every `jwt.verify` call without an explicit `algorithms` parameter regardless of library version.

Related rules + further reading

Mesrai rule pack: security/jwt-algorithms-pinned — flags `jwt.verify` without `algorithms` allowlist.

OWASP: JSON Web Token Cheat Sheet.

CWE-347 — Improper Verification of Cryptographic Signature.

Takeaway

Pin the algorithm. Pin the issuer. Pin the audience. Three options, two seconds to write, full class of JWT bypass closed.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.