Guide Updated

JWT kid Header Injection

JWT kid header injection lets an attacker use SQL injection or path traversal via the kid parameter to force the verifier into using an attacker-controlled key. Learn the attack and the allowlist fix.

High

The `kid` (key ID) header parameter tells the verifier which signing key to use. It is designed for key rotation. The JWT specification places no restrictions on the format of `kid` values: they can be integers, UUIDs, file paths, or any string. If the server uses `kid` to look up a key from a database or filesystem without sanitizing it, the value becomes an injection vector entirely controlled by the attacker.

How the attack works

The attacker controls the `kid` header (it is part of the token, which the attacker constructs). If the server interpolates `kid` into a SQL query (`SELECT public_key FROM signing_keys WHERE id = '<kid>'`), the attacker sets `kid` to a SQL injection string like `' UNION SELECT 'attacker-controlled-secret' --`, the query returns `attacker-controlled-secret` as the key, and the attacker signed their token with that same string: verification passes. If the server builds a file path from `kid` (`/var/keys/<kid>.pem`), the attacker sets `kid` to `../../dev/null`; on Linux, reading `/dev/null` returns an empty buffer, and HMAC with an empty key produces a predictable output the attacker can compute themselves.

The fix

Validate `kid` values against a strict allowlist before any lookup. Never interpolate `kid` into SQL strings or file paths. If you must use a database lookup, use parameterized queries: the `kid` goes into the parameter slot, not the query string, so SQL injection is impossible regardless of what `kid` contains.

Code examples

The attack (SQL injection via kid)

The attack (SQL injection via kid)
// Vulnerable server code
app.post("/api/resource", async (req, res) => {
  const token = req.headers.authorization.split(" ")[1];
  const header = JSON.parse(
    Buffer.from(token.split(".")[0], "base64url").toString()
  );

  // kid is attacker-controlled from the token header
  const result = await db.query(
    `SELECT public_key FROM signing_keys WHERE id = '${header.kid}'`
  );
  jwt.verify(token, result.rows[0].public_key, { algorithms: ["RS256"] });
});

// Attacker sets kid to:
//   ' UNION SELECT 'attacker-controlled-secret' --

The fix (allowlist + parameterized query)

The fix (allowlist + parameterized query)
// Allowlist approach: recommended
const VALID_KEY_IDS = new Set(["key-v1", "key-v2", "key-v3"]);

function getSigningKey(kid) {
  if (!VALID_KEY_IDS.has(kid)) {
    throw new Error(`Unknown key ID: ${kid}`);
  }
  return keyStore.get(kid);
}

// Or parameterized query: safe
const result = await db.query(
  "SELECT public_key FROM signing_keys WHERE id = $1",
  [header.kid]
);

Frequently asked questions

  • What is JWT kid injection?

    kid injection is an attack where the attacker uses the kid (key ID) header parameter: which is attacker-controlled, since it is part of the token: to inject SQL or path traversal into the verifier's key lookup. If the server interpolates kid into a SQL query or file path without sanitizing it, the attacker forces the verifier to use an attacker-controlled key, then signs a forged token with that same key so verification passes. The fix is to validate kid against a strict allowlist and use parameterized queries.

  • How do I prevent JWT kid injection?

    Validate kid values against a strict allowlist of known key IDs before any lookup. Never interpolate kid into SQL strings or file paths. If you use a database lookup, use parameterized queries (the kid goes into the parameter slot, not the query string). If you use a filesystem lookup, restrict kid to a known set of filenames. The kid value is attacker-controlled input and must be treated as untrusted at every boundary.

  • What does the kid header parameter do in a JWT?

    kid (key ID) is an optional JOSE header parameter that tells the verifier which signing key to use when the issuer has multiple active keys. It is designed for key rotation: when old and new keys are both valid, kid identifies which one signed this token so the verifier picks the right one. The JWT specification places no restrictions on kid's format, which is why unsanitized kid values become an injection vector.

Related