Back to Blog
high SEVERITY7 min read

How Quadratic CPU Consumption Happens in JavaScript YAML Parsing and How to Fix It

A high-severity denial-of-service vulnerability in js-yaml (GHSA-5p4m-2wfm-xmqj) allowed attackers to trigger quadratic CPU consumption by supplying crafted YAML input containing `!!omap` (ordered map) types. The vulnerability affected both the 3.x and 4.x branches of js-yaml, and the fix for CVE-2026-59870 had not been backported to all affected versions. Upgrading from `js-yaml@4.3.0` to `4.3.1` (and `3.15.0` to `3.15.1`) resolves the issue by correcting the inefficient duplicate-key detection

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

GHSA-5p4m-2wfm-xmqj is a high-severity Denial of Service vulnerability in js-yaml (CWE-407) where parsing a YAML document containing a crafted `!!omap` (ordered map) tag triggers quadratic CPU consumption due to an O(n²) duplicate-key detection loop. Affecting both js-yaml 3.x and 4.x, the vulnerability is fixed by upgrading to js-yaml 4.3.1 (or 3.15.1), which replaces the nested-loop duplicate check with an efficient Set-based O(n) lookup. In the affected project, this required bumping the version constraint in both `package.json` and `package-lock.json` from `^4.3.0` to `^4.3.1`.

Vulnerability at a Glance

cweCWE-407 (Inefficient Algorithmic Complexity)
fixUpgrade js-yaml to 4.3.1 / 3.15.1, which uses an O(n) Set-based duplicate check
riskAttacker-supplied YAML with a large !!omap can pin a CPU core, causing service unavailability
languageJavaScript / Node.js
root causeO(n²) nested-loop duplicate-key check in the !!omap type resolver in js-yaml ≤4.3.0 / ≤3.14.x
vulnerabilityQuadratic CPU Consumption (Algorithmic Complexity / ReDoS-class DoS)

How Quadratic CPU Consumption Happens in JavaScript YAML Parsing and How to Fix It

Introduction

The package-lock.json file in this Node.js application pins js-yaml at version 4.3.0 — a version that harbors a subtle but dangerous algorithmic flaw. When the YAML parser encounters a !!omap (ordered map) tag, it validates that no duplicate keys exist by iterating over every previously seen key for each new key it processes. That nested loop is O(n²): a crafted YAML document with 10,000 ordered-map entries forces roughly 50 million comparisons before parsing completes. An attacker who can supply YAML to any endpoint that calls js-yaml can weaponize this behaviour to pin a CPU core and deny service to legitimate users.

This post dissects exactly how the !!omap resolver creates that quadratic path, shows the one-line dependency bump that closes it, and explains what developers should watch for in their own YAML-processing code.


The Vulnerability Explained

What is !!omap and why does it need duplicate checking?

YAML's !!omap tag represents an ordered mapping — a sequence of single-entry mappings that preserves insertion order while still requiring unique keys. The js-yaml library implements this as a custom type resolver. During resolution, it must verify that no key appears more than once.

In js-yaml ≤ 4.3.0 (and ≤ 3.14.x), the duplicate-key check was implemented with a pattern equivalent to:

// Pseudocode reflecting the vulnerable logic in js-yaml ≤ 4.3.0
function resolveOmap(data) {
  const pairs = parsePairs(data);        // n entries
  for (let i = 0; i < pairs.length; i++) {
    const key = pairs[i][0];
    for (let j = 0; j < i; j++) {       // ← inner loop grows with i
      if (pairs[j][0] === key) {
        throw new YAMLException('duplicate key in !!omap');
      }
    }
  }
  return pairs;
}

The inner loop compares the current key against all previously seen keys. For n entries, the total number of comparisons is 1 + 2 + 3 + … + (n-1) = n(n-1)/2 — classic O(n²) behaviour.

Crafting the attack

An attacker needs nothing more than a YAML document like this:

!!omap
- key_0001: value
- key_0002: value
- key_0003: value
# ... 10,000 more unique keys ...
- key_9999: value

Because every key is unique, the parser never throws an exception — it grinds through all ~50 million comparisons before returning a result. On a modern server, parsing a 10,000-entry !!omap can consume several seconds of CPU time per request. A handful of concurrent requests is enough to saturate a single-core worker process.

Why this application is at risk

The package.json in this repository lists js-yaml as a direct, production dependency ("js-yaml": "^4.3.0"), sitting alongside better-sqlite3, dompurify, and pg. Any code path that calls yaml.load(), yaml.loadAll(), or yaml.safeLoad() on externally supplied content — configuration uploads, API payloads, webhook bodies — is a potential trigger. Because dompurify is also present, this appears to be a full-stack web application where user-generated content is a realistic attack surface.


The Fix

What changed

The fix is a two-file dependency bump:

package.json — tighten the minimum required version:

-    "js-yaml": "^4.3.0",
+    "js-yaml": "^4.3.1",

package-lock.json — pin the resolved version and update the integrity hash:

     "node_modules/js-yaml": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
-      "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+      "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",

Both files must be updated together: package.json ensures npm install never resolves back to 4.3.0, and package-lock.json ensures the exact resolved URL and integrity hash match the patched release, preventing supply-chain substitution.

How js-yaml 4.3.1 fixes the algorithm

The patched release replaces the O(n²) nested loop with an O(n) Set-based lookup:

// Pseudocode reflecting the fixed logic in js-yaml 4.3.1
function resolveOmap(data) {
  const pairs = parsePairs(data);
  const seenKeys = new Set();            // ← O(1) lookup
  for (const [key] of pairs) {
    if (seenKeys.has(key)) {
      throw new YAMLException('duplicate key in !!omap');
    }
    seenKeys.add(key);
  }
  return pairs;
}

A Set.has() call is O(1) on average. The total work is now O(n) — doubling the number of entries doubles the work, not quadruples it. A 10,000-entry !!omap that previously required ~50 million comparisons now requires exactly 10,000 hash lookups.

Behaviour preservation

Valid YAML documents — including those with !!omap tags — parse identically after the upgrade. The only observable difference is that maliciously large inputs are rejected or processed in linear time rather than quadratic time.


Prevention & Best Practices

1. Audit all YAML entry points for untrusted input

Any call to yaml.load() that accepts externally supplied data is a potential DoS vector. Even with the algorithmic fix in place, consider adding a size cap before parsing:

import yaml from 'js-yaml';

const MAX_YAML_BYTES = 1_000_000; // 1 MB

function safeParseYaml(raw) {
  if (Buffer.byteLength(raw, 'utf8') > MAX_YAML_BYTES) {
    throw new Error('YAML payload exceeds maximum allowed size');
  }
  return yaml.load(raw);
}

2. Keep dependency lock files in version control and CI

The package-lock.json change is as important as the package.json change. Without committing the lock file, npm ci cannot guarantee the patched version is installed. Enforce npm ci (not npm install) in CI pipelines to honour the lock file exactly.

3. Run automated dependency scanning on every PR

Tools like Trivy (which detected this vulnerability), npm audit, Dependabot, and Snyk can flag known CVEs in package-lock.json before they reach production. Configure them to fail the build on HIGH or CRITICAL findings.

4. Watch for O(n²) patterns in custom type resolvers

If your project defines custom YAML types via js-yaml's Type API, audit the resolve, construct, and represent callbacks for nested loops over attacker-controlled collections. Prefer Map and Set over array-scan patterns.

5. Reference standards


Key Takeaways

  • The !!omap resolver in js-yaml ≤ 4.3.0 uses an O(n²) duplicate-key scan — a single crafted YAML document with thousands of ordered-map entries is enough to saturate a CPU core.
  • Both package.json and package-lock.json must be updated together — updating only one leaves a gap where the wrong version can be resolved during npm install.
  • The integrity hash in package-lock.json changed (sha512-1td788...sha512-CY6crG...) — this is the cryptographic proof that the installed package matches the patched release, not the vulnerable one.
  • Algorithmic DoS is distinct from traditional ReDoS — there is no regular expression involved; the attack surface is the YAML type-resolution pipeline itself, making regex-focused scanners insufficient on their own.
  • Linear-time alternatives (Set, Map) should always replace nested-loop duplicate checks over attacker-controlled data, regardless of the language or framework.

How Orbis AppSec Detected This

  • Source: Externally supplied YAML content parsed by js-yaml — any HTTP request body, file upload, or configuration payload that reaches yaml.load() or yaml.loadAll().
  • Sink: The !!omap type resolver inside node_modules/js-yaml (resolved to version 4.3.0 via package-lock.json), which performs an O(n²) duplicate-key scan on attacker-controlled key arrays.
  • Missing control: No algorithmic complexity bound on the !!omap duplicate-key detection loop; no upstream patch (CVE-2026-59870) applied to the pinned version.
  • CWE: CWE-407 — Inefficient Algorithmic Complexity.
  • Fix: Bumped js-yaml from 4.3.0 to 4.3.1 in both package.json and package-lock.json, replacing the O(n²) loop with an O(n) Set-based check.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

GHSA-5p4m-2wfm-xmqj is a reminder that security vulnerabilities do not always look like memory corruption or injection flaws. A quietly inefficient algorithm hidden inside a YAML type resolver can be just as dangerous as an SQL injection when it sits on the path of untrusted input. The fix — a one-version bump from js-yaml@4.3.0 to 4.3.1 — is minimal, non-breaking, and eliminates the quadratic work entirely by swapping a nested loop for a Set lookup.

The broader lesson: treat your package-lock.json as a first-class security artefact. Keep it committed, keep it scanned, and keep it current. Automated tools like Orbis AppSec can watch that file continuously and open targeted, context-rich pull requests the moment a patched version is available — before an attacker finds the gap.


References

Frequently Asked Questions

What is quadratic CPU consumption in YAML parsing?

It is a class of Denial of Service where the time to parse a document grows as O(n²) — doubling the input size quadruples the work — allowing a small crafted payload to exhaust CPU resources.

How do you prevent algorithmic complexity attacks in JavaScript?

Audit custom type resolvers and collection-processing loops for nested iterations over attacker-controlled data; prefer Set or Map lookups (O(1)) over linear scans (O(n)) inside loops.

What CWE is quadratic CPU consumption?

CWE-407: Inefficient Algorithmic Complexity — a subset of resource-exhaustion weaknesses that covers non-linear time or space growth triggered by crafted inputs.

Is input length-limiting enough to prevent this vulnerability?

Partially — it raises the bar but does not eliminate the risk, because quadratic growth means even moderately sized inputs (thousands of keys) can cause measurable CPU spikes. The correct fix is the algorithmic change in js-yaml 4.3.1.

Can static analysis detect quadratic CPU consumption?

Specialized tools (e.g., CodeQL's "quadratic time complexity" queries, Semgrep rules targeting nested loops over the same collection) can flag suspicious patterns, but the most reliable signal is upgrading to a patched dependency version.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4

Related Articles

critical

How Denial of Service via Gzip Bomb happens in Node.js tar and how to fix it

A critical Denial of Service vulnerability (CVE-2026-59873) was discovered in the `tar` npm package at version 2.2.2, used in the frontend dependency tree. An attacker could craft a malicious gzip bomb that, when processed by node-tar, would expand to consume all available memory and crash the application. The fix upgrades `tar` from the legacy 2.2.2 to 7.5.19, which includes decompression limits and removes the vulnerable `block-stream` dependency.

high

How Denial of Service via Memory Exhaustion happens in Socket.IO Parser and how to fix it

A high-severity denial of service vulnerability (CVE-2026-69185) was discovered in socket.io-parser versions prior to 4.2.7, 3.4.5, and 3.3.6. The flaw allowed attackers to exhaust server memory through specially crafted packets, potentially crashing real-time communication services. The fix involved upgrading the socket.io-parser dependency in the react-dashboard component to the patched version 4.2.7.

critical

How Command Injection Vulnerabilities Happen in Python Subprocess Calls and How to Fix Them

A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.

critical

How Rate Limiting Vulnerabilities Happen in Node.js OAuth Endpoints and How to Fix Them

A critical resource exhaustion vulnerability was discovered in the OAuth token endpoint at `server/routes/oauth.js`. Without rate limiting, attackers could flood the `/api/oauth/token` endpoint with requests, each triggering expensive bcrypt verification operations that would exhaust server CPU and memory. The fix implements per-IP rate limiting using `express-rate-limit` to cap requests at 20 per 15-minute window.

critical

How Unvalidated External Content Fetching happens in Python Build Scripts and how to fix it

A Python build script in the NUR (Nix User Repository) project was fetching external content from GitHub without implementing response integrity validation or proper error handling. While TLS verification was enabled by default, the absence of timeout controls, status code validation, and integrity checks left the build pipeline vulnerable to man-in-the-middle attacks and denial-of-service conditions that could compromise the generated static site content.

high

How Denial of Service Attacks Happen in PHP Markdown Parsers and How to Fix Them

The league/commonmark library contained a denial of service vulnerability in its Attributes extension that could be triggered by specially crafted markdown with distinctly-named attributes. This vulnerability was fixed in version 2.10.0 by addressing how attribute names are processed during markdown parsing, preventing attackers from exhausting server resources.