JWK Injection

Embed your own RSA public key in the JWT header. A vulnerable server uses it to verify - against the attacker's own key.

Embedded keys in the RFC

RFC 7515 §4.1.3 defines the jwk (JSON Web Key) header parameter as:"The public key that corresponds to the key used to digitally sign the JWS."The intent is for the token to be self-describing - a recipient can fetch the public key directly from the token without contacting any external endpoint.

The vulnerability arises when a library or application uses the embedded JWK directly for signature verification without checking it against a trusted key store. Since the attacker controls the token - including its header - they can embed any public key they choose, sign the token with the matching private key, and the server will successfully verify the signature using the attacker's own key.

The forged token structure

Forged token header with embedded attacker JWK
{
  "alg": "RS256",
  "typ": "JWT",
  "jwk": {
    "kty": "RSA",
    "use": "sig",
    "alg": "RS256",
    "kid": "attacker-key-2024",
    "n":   "sdfG7kPqoK8zRx...",   // attacker's public key modulus (base64url)
    "e":   "AQAB"                  // public exponent (65537)
  }
}

The payload contains whatever claims the attacker wants (role: "admin", arbitrary sub, etc.). The signature is computed using the attacker's RSA private key. A vulnerable server:

  • Parses the header and extracts the jwk field
  • Imports the embedded public key
  • Verifies the signature using that key
  • The signature is valid - it was signed by the corresponding private key
  • The server accepts the forged token as legitimate

node-jose and CVE-2018-0114

The most widely-cited real-world instance is node-jose versions prior to 0.11.0, disclosed in 2018. node-jose is the JOSE implementation used internally by many enterprise products, notably several Cisco applications. The vulnerable code path:

Vulnerable node-jose code path
const jose = require('node-jose');

// VULNERABLE: key is extracted from the token header itself
async function verifyToken(token) {
  // node-jose < 0.11.0 trusted the embedded JWK without validation
  const key = await jose.JWK.asKey(
    JSON.parse(atob(token.split('.')[0])).jwk  // ← attacker-controlled
  );
  const result = await jose.JWS.createVerify(key).verify(token);
  return result.payload;
}

// Attacker supplies a token with their own key in the header.
// jose.JWK.asKey() imports it.
// jose.JWS.createVerify(key).verify(token) - verifies against attacker's key → pass.
Secure - verify against a trusted keystore
const jose = require('node-jose');

// Build a keystore from pre-configured trusted keys ONLY
const keystore = jose.JWK.createKeyStore();
await keystore.add(trustedPublicKeyJwk);  // server-side trusted key

async function verifyToken(token) {
  // createVerify(keystore) IGNORES any embedded jwk/jku header params
  // and only uses keys from the trusted store
  const result = await jose.JWS.createVerify(keystore).verify(token);
  return result.payload;
}
Cisco Security Advisory cisco-sa-20190513-securelogin
CVE-2018-0114 affected Cisco products using node-jose as their JOSE implementation. This included authentication components in Cisco products that accepted self-signed JWTs. The advisory rated it Critical (CVSS 10.0 in some configurations) as it allowed complete authentication bypass.

EC key variant

The same attack works with ECDSA (ES256). The attacker generates an EC keypair, embeds the public EC key as the jwk claim with kty: "EC", and signs with the private EC key. The JWK format for EC keys:

EC public key embedded in header
{
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
    "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
  }
}

Detection

To test a target, generate a fresh RSA or EC keypair in JWT Arsenal, forge a token with an arbitrary payload and your public key embedded in the header, then send it:

Quick test with jwt_tool
# jwt_tool's -X i flag performs JWK injection automatically:
python3 jwt_tool.py <JWT> -X i

# To inject with a specific claim:
python3 jwt_tool.py <JWT> -X i -I -pc sub -pv admin
CVE-2018-0114 and beyond
  • node-jose < 0.11.0 - CVSS 10.0 in Cisco product context
  • Affected Cisco Prime Infrastructure, Cisco Enterprise Network Function Virtualization Infrastructure Software, and others
  • Pattern still appears in custom JWT parsers that extract the JWK before validating the signature
  • Also found in some OIDC client libraries that support key discovery via jwk header

Mitigations

  • Never use the jwk header field as the source of the verification key - always use a server-side trusted key store
  • If the library supports "keystore mode" vs "auto-key mode," always use keystore mode
  • Validate that any key material used for verification matches a pre-configured trusted key by key ID or thumbprint
  • Consider stripping or rejecting tokens that contain jwk, jku, or x5u header claims before processing
  • Keep JWT libraries up to date - this class of bug is typically fixed in library updates
KID InjectionJKU Injection
GitHub
JWT Arsenal_
Loading cryptographic engineOK
Importing exploit modulesOK
Verifying secure contextOK
All systems operational
100% CLIENT-SIDE · NO DATA LEAVES YOUR BROWSER