Back to Cases
MediumCWE-1321

Prototype Pollution Disables Low-S Signature Malleability Protection

noble-secp256k12026-02-23CWE-1321Medium

CVSS v4.0

Score: 6.3 Vector: CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:H/SA:N

Rationale: The vulnerability requires a prototype pollution primitive as a precondition (AT:P) and achieving prototype pollution itself adds attack complexity (AC:H). While the integrity impact is high on both the vulnerable system (signature verification bypass) and subsequent systems (cryptocurrency transaction malleability), the dual prerequisites bring the overall score to Medium. In cryptocurrency-specific deployments, the contextual severity may be higher due to the concrete exploitability of signature malleability in those environments.

CWE

CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')

Affected Files

  • index.ts:719setDefaults function reads options via bracket notation without hasOwnProperty guard
  • index.ts:723opts[k] ?? defaultSignOpts[k] uses nullish coalescing, which does NOT guard against false

Description

The setDefaults function merges user-provided options with default values using the nullish coalescing operator (??). This operator only falls back to the default when the left operand is null or undefined — it does NOT fall back for false, 0, or ''.

When options are read via opts[k], JavaScript's prototype chain is traversed. If an attacker achieves prototype pollution (e.g., Object.prototype.lowS = false), the polluted value false will be picked up instead of the intended default true. Since false ?? true evaluates to false, the security-critical lowS enforcement is silently disabled.

With lowS disabled, the verify() function accepts high-S signatures (s > N/2). ECDSA signatures are inherently malleable: for any valid signature (r, s), the pair (r, N-s) is also valid. This malleability is a known attack vector in Bitcoin (pre-BIP 66) and Ethereum (EIP-2), where it can be exploited to alter transaction IDs without invalidating the signature.

The same prototype pollution vector also affects prehash (could disable SHA-256 pre-hashing of messages) and format (could change expected signature format, causing verification failures as a DoS).

Reproduction Steps

  1. Set up a Node.js environment with noble-secp256k1 installed, and configure the sync hash functions.
  2. Run the following code:
import * as secp from '@noble/secp256k1';
import { sha256 } from '@noble/hashes/sha2';
import { hmac } from '@noble/hashes/hmac';

secp.hashes.sha256 = sha256;
secp.hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);

// Generate key pair
const secretKey = secp.utils.randomSecretKey();
const publicKey = secp.getPublicKey(secretKey);

// Sign a message with lowS: false to get a high-S signature
const msg = new Uint8Array(32).fill(0x42);
const highSSig = secp.sign(msg, secretKey, { lowS: false });
const sig = secp.Signature.fromBytes(highSSig);

// Verify that with defaults, high-S is rejected
console.log('Before pollution, high-S accepted:', secp.verify(highSSig, msg, publicKey));
// Expected output: false

// Simulate prototype pollution (could come from any vulnerable dependency)
Object.prototype.lowS = false;

// Now verify again — high-S is accepted
console.log('After pollution, high-S accepted:', secp.verify(highSSig, msg, publicKey));
// Expected output: true  <-- VULNERABILITY: malleability protection bypassed

// Clean up
delete Object.prototype.lowS;
  1. Observe that after prototype pollution, verify() accepts malleable (high-S) signatures that should be rejected.

Impact

An attacker who can achieve prototype pollution in the same JavaScript context (common via deserialization vulnerabilities, JSON.parse with unsafe merge, or vulnerable dependencies) can:

  1. Bypass signature malleability protection: High-S signatures are accepted, enabling transaction malleability attacks in Bitcoin/Ethereum applications.
  2. Disable message pre-hashing: If prehash is polluted to false, signatures are computed and verified on raw messages instead of SHA-256 hashes, which could lead to application-level signature confusion.
  3. Cause verification DoS: Polluting format can cause legitimate signatures to be rejected if the expected length changes.

Suggested Fix

Use Object.hasOwn(opts, k) or Object.prototype.hasOwnProperty.call(opts, k) to check whether the property is directly on the options object before reading it:

const setDefaults = (opts: ECDSASignOpts): Required<ECDSASignOpts> => {
  const res: ECDSASignOpts = {};
  Object.keys(defaultSignOpts).forEach((k: string) => {
    // @ts-ignore
    res[k] = Object.hasOwn(opts, k) ? opts[k] : defaultSignOpts[k];
  });
  return res as Required<ECDSASignOpts>;
};