Improper JWT signature verification

Improper JWT signature verification through the none algorithm

Description

Allowing the unsigned JWT algorithm none for authentication can make an application accept attacker-modified payloads. In jsonwebtoken 9.x, an empty key alone does not automatically skip signature verification. The before example explicitly allows none in algorithms alongside an empty key. Trusting identity or permission claims without verifying a signature may enable authentication or authorization bypass.

Potential impact

  • Forged tokens may impersonate an authenticated user.
  • Altered role or admin claims may grant excessive permissions.
  • An attacker may access protected APIs and sensitive data.
  • Impersonation may allow ongoing use of another user's resources.

Remediation

  • Always supply a valid trusted secret or public key to verify().
  • Specify an algorithm allow-list and exclude 'none', for example { algorithms: ["HS256"] } or { algorithms: ["RS256"] }.
  • decode() only parses a token. Do not trust its result for authentication or authorization; use verify() with a trusted key.
  • Validate claims such as iss (issuer), aud (audience), and exp (expiration). If expiration is required, also require the exp claim to be present.

Examples

Before

javascript
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();

// Before: verify without a key (for example, an empty string)
app.get("/profile", (req, res) => {
  const auth = req.headers.authorization || "";
  const token = auth.replace(/^Bearer\s+/i, "");

  // BAD: an empty key and explicit none allowance permit unsigned tokens
  const payload = jwt.verify(token, "", { algorithms: ["HS256", "none"] });

  // Forged payloads may gain access
  res.json({ user: payload.sub, admin: payload.admin });
});

app.listen(3000);

After

javascript
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();

const JWT_SECRET = process.env.JWT_SECRET; // Store securely

app.get("/profile", (req, res) => {
  const m = (req.headers.authorization || "").match(/^Bearer\s+(.+)$/i);
  if (!m) return res.status(401).send("Missing token");

  try {
    const payload = jwt.verify(m[1], JWT_SECRET, {
      algorithms: ["HS256"], // Allow only the required algorithm, excluding none
      issuer: "auth.example.com", // Validate iss
      audience: "my-api", // Validate aud
    });
    return res.json({ user: payload.sub });
  } catch (e) {
    return res.status(401).send("Invalid token");
  }
});

app.listen(3000);

Explanation:

  • Before: An empty key combined with none in algorithms permits unsigned tokens. An attacker-controlled payload may be trusted as authenticated identity data.
  • After: A trusted key and a restricted algorithm list exclude none. The handler also checks iss and aud and rejects validation failures. Configure JWT_SECRET with a strong secret. Expiration is checked when exp is present, but this excerpt does not require that claim to exist.

References