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

high

How Unicode Hostname Canonicalization Bypass happens in Node.js and how to fix it

CVE-2026-13676 is a high-severity vulnerability in the `fast-uri` npm package where improper Unicode hostname canonicalization allowed attackers to bypass security policies by crafting hostnames that appeared safe but resolved differently after normalization. The fix upgrades `fast-uri` from version 3.1.2 to 4.1.2 and pins the version using an npm `overrides` directive in `package.json` to ensure no transitive dependency pulls in the vulnerable version.

high

How Security Policy Bypass via Improper Unicode Hostname Canonicalization Happens in Node.js and How to Fix It

A high-severity vulnerability (CVE-2026-13676) in the `fast-uri` npm package allowed attackers to bypass security policies through improper Unicode hostname canonicalization. The fix upgrades `fast-uri` from version 3.1.0 to 4.1.2 using npm overrides to ensure the patched version is used throughout the entire dependency tree of the `ide-agent-kit` project.

high

How ReDoS happens in Node.js path-to-regexp and how to fix it

CVE-2024-52798 is a Regular Expression Denial of Service (ReDoS) vulnerability in the `path-to-regexp` package's 0.1.x branch, which remains unpatched in that legacy line. Because `path-to-regexp` is a transitive dependency pulled in by `websocket-driver` and many other popular Node.js packages, any application that processes attacker-controlled URL paths through an affected version is at risk of catastrophic backtracking that can freeze the event loop. Upgrading `websocket-driver` to 0.7.5 — an

high

How Denial of Service via Crafted Long-Path Tar Archives Happens in Node.js and How to Fix It

CVE-2026-73566 is a Denial of Service vulnerability in node-tar that allows attackers to craft specially malformed tar archives with excessively long file paths to exhaust system resources and crash applications. The fix upgrades tar from version 7.5.19 to 7.5.21, which implements proper path length validation to prevent this attack vector.

high

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

CVE-2026-69185 is a high-severity Denial of Service vulnerability in the `socket.io-parser` package that allows attackers to exhaust server memory by sending specially crafted packets. The fix upgrades `socket.io-parser` from version 4.2.4 to 4.2.7 (and parallel branches to 3.4.5 and 3.3.6) in `client/package-lock.json`, closing the attack surface against malicious clients. This kind of memory-exhaustion flaw is particularly dangerous in real-time applications where the parser handles a continuo

high

How express-check-csurf-middleware-usage happens in JavaScript/Express and how to fix it

A high-severity CSRF vulnerability was identified in `tower_game/index.js` where the Express application lacked any Cross-Site Request Forgery protection middleware. Without CSRF validation, an attacker could craft malicious pages that trick authenticated users into submitting unwanted requests to the game server. The fix adds `csurf` middleware with cookie-based token storage in just four lines of code.