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) caused quadratic CPU consumption when resolving `!!omap` YAML types in both the 3.x and 4.x branches. The fix upgrades js-yaml from 3.14.2 to 3.15.1 and from 4.1.1 to 4.3.1, eliminating the algorithmic complexity exploit while leaving all valid YAML inputs unaffected.

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

Answer Summary

GHSA-5p4m-2wfm-xmqj is a high-severity algorithmic complexity vulnerability (CWE-407) in the js-yaml JavaScript library, affecting both the 3.x and 4.x branches. When parsing YAML documents containing the `!!omap` (ordered map) type, the library's resolution logic exhibits O(n²) CPU growth, allowing an attacker to craft a small YAML payload that consumes disproportionate CPU resources and causes a denial of service. The fix upgrades js-yaml to version 4.3.1 (from 4.1.1) and 3.15.1 (from 3.14.2), and removes the pinned vulnerable 3.14.2 sub-dependency nested under `@istanbuljs/load-nyc-config` in `package-lock.json`.

Vulnerability at a Glance

cweCWE-407 (Inefficient Algorithmic Complexity)
fixUpgrade js-yaml to 4.3.1 and 3.15.1, which rewrites the omap resolver with linear duplicate detection
riskAn attacker supplying a crafted YAML document with a large `!!omap` node can stall or crash a Node.js service
languageJavaScript / Node.js
root causeThe `!!omap` type resolver in js-yaml 3.x/4.x used a nested duplicate-key check with O(n²) time complexity
vulnerabilityQuadratic CPU consumption (Algorithmic Complexity / ReDoS-class DoS)

The Hidden Cost of Parsing YAML: A Quadratic Trap in js-yaml

In a Node.js project's package-lock.json, a routine dependency audit surfaced something easy to overlook: a pinned sub-dependency deep in the @istanbuljs/load-nyc-config subtree was pulling in js-yaml version 3.14.2 — a version containing a high-severity denial-of-service flaw that had never been backported with the upstream fix. Meanwhile, the top-level js-yaml dependency sat at 4.1.1, itself also vulnerable. The result: two separate vulnerable instances of the same library living in the same dependency tree, both capable of being triggered by a single crafted YAML document.

This is the story of GHSA-5p4m-2wfm-xmqj — a deceptively simple algorithmic flaw in !!omap resolution that can bring a Node.js service to its knees.


The Vulnerability Explained

What is !!omap and why does it matter?

YAML's !!omap (ordered map) type represents a sequence of key-value pairs where insertion order is preserved and duplicate keys are explicitly forbidden. When js-yaml encounters a !!omap node, it must validate that no key appears more than once. In the vulnerable versions (js-yaml 3.x through 3.14.x and 4.x through 4.1.x), this duplicate-key check was implemented using a nested loop — for every new key encountered, the resolver iterated over all previously seen keys to check for a match.

This is the classic O(n²) pattern:

// Conceptual representation of the vulnerable duplicate-check logic
// in js-yaml's !!omap resolver (pre-fix)
function resolveOmap(data) {
  for (let i = 0; i < data.length; i++) {
    for (let j = 0; j < i; j++) {
      if (data[i].key === data[j].key) {
        throw new Error('duplicate key');
      }
    }
  }
}

For a YAML !!omap with n entries, this performs approximately n²/2 comparisons. With 10,000 entries, that's ~50 million comparisons. With 100,000 entries, it's ~5 billion. The CPU time grows quadratically while the input size only grows linearly.

The Concrete Attack Scenario

An attacker who can supply YAML input to any code path that calls js-yaml's safeLoad(), load(), or parse() functions can craft a payload like:

!!omap
- key_0000001: value
- key_0000002: value
- key_0000003: value
# ... repeated 50,000 times with unique keys
- key_0050000: value

Because all keys are unique, the parser never throws a duplicate error — it simply grinds through all O(n²) comparisons before returning the parsed object. A single HTTP request carrying such a payload could monopolize a Node.js event loop thread for seconds or minutes, effectively denying service to all other requests.

Why Two Vulnerable Versions Were Present

The diff reveals a particularly important detail: the vulnerability existed in two separate locations in the dependency tree:

  1. Top-level: js-yaml at version 4.1.1
  2. Nested under @istanbuljs/load-nyc-config: a pinned js-yaml at version 3.14.2
// BEFORE — vulnerable nested dependency in package-lock.json
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
  "version": "3.14.2",
  "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
  "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/...",
  "dev": true,
  "dependencies": {
    "argparse": "^1.0.7",
    "esprima": "^4.0.0"
  }
}

The nested 3.14.2 instance was pinned because @istanbuljs/load-nyc-config required the older js-yaml 3.x API. Even though this dependency is marked "dev": true, it is still a real attack surface during CI/CD pipelines and development environments — and in some build configurations, dev dependencies are inadvertently bundled into production artifacts.


The Fix

The fix made two coordinated changes:

1. Upgrade the top-level js-yaml (4.1.1 → 4.3.1)

package.json was updated to require js-yaml@^4.3.1, and package-lock.json was regenerated to reflect the resolved 4.3.1 version. Version 4.3.1 replaces the nested loop duplicate-key check with a Set-based O(n) lookup:

// AFTER — linear duplicate detection using a Set (js-yaml 4.3.1)
function resolveOmap(data) {
  const seen = new Set();
  for (const item of data) {
    if (seen.has(item.key)) {
      throw new Error('duplicate key');
    }
    seen.add(item.key);
  }
}

A Set lookup is O(1) amortized, making the entire loop O(n). A 100,000-entry !!omap now requires 100,000 operations instead of 5 billion.

2. Remove the pinned vulnerable 3.14.2 sub-dependency

The diff removes the entire nested node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml block (along with its argparse and sprintf-js companions) from package-lock.json:

-    "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
-      "version": "3.14.2",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
-      "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+...",
-      "dev": true,
-      "dependencies": {
-        "argparse": "^1.0.7",
-        "esprima": "^4.0.0"
-      }
-    },
-    "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { ... },
-    "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { ... },
-    "node_modules/esprima": { ... },

By removing the nested override, @istanbuljs/load-nyc-config is now resolved to use the hoisted, patched js-yaml rather than its own pinned vulnerable copy. The esprima package (a dependency of js-yaml 3.x that is no longer needed by the patched version) is also removed, trimming unnecessary weight from the dependency tree.

Before and After at a Glance

Location Before After
Top-level js-yaml 4.1.1 (vulnerable) 4.3.1 (patched)
@istanbuljs nested js-yaml 3.14.2 (vulnerable) Removed (hoists to patched version)
esprima 4.0.1 (orphaned dep) Removed

Prevention & Best Practices

1. Audit transitive dependencies, not just direct ones

This vulnerability existed in a nested dependency that would not have been caught by only inspecting package.json. Always audit package-lock.json or yarn.lock for vulnerable versions:

# Using npm audit
npm audit

# Using Trivy for comprehensive SCA
trivy fs --scanners vuln .

2. Treat dev dependencies as a real attack surface

The vulnerable js-yaml@3.14.2 was marked "dev": true, but dev dependencies can:
- Be exploited in CI/CD pipelines
- Be accidentally bundled into production builds
- Introduce supply-chain risks via postinstall scripts

Apply the same patching discipline to dev dependencies as to production ones.

3. Use npm dedupe after upgrades

After resolving version conflicts, run:

npm dedupe

This collapses redundant nested copies of packages into a single hoisted version, reducing both attack surface and bundle size.

4. Set upper bounds cautiously, lower bounds firmly

Avoid pinning exact versions of transitive dependencies in package.json unless absolutely necessary. Prefer semver ranges (^4.3.1) that allow patch updates to flow through automatically.

5. Relevant Standards

  • CWE-407: Inefficient Algorithmic Complexity — the root cause of this vulnerability
  • OWASP A06:2021 – Vulnerable and Outdated Components — keeping dependencies patched is a top-10 security priority
  • OWASP Denial of Service Cheat Sheet — guidance on preventing resource exhaustion attacks

Key Takeaways

  • The !!omap type resolver in js-yaml ≤3.14.x and ≤4.1.x uses O(n²) duplicate-key detection — a single large YAML document can exhaust CPU on the Node.js event loop thread.
  • Nested dependency pins in package-lock.json can silently reintroduce patched-out vulnerabilities — the @istanbuljs/load-nyc-config subtree was carrying 3.14.2 even after the top-level dependency was upgraded.
  • Removing the nested js-yaml@3.14.2 block also eliminated esprima@4.0.1 — a previously required but now-unnecessary parser dependency, reducing the overall attack surface.
  • Dev-only dependencies are not safe to ignore — the vulnerable instance was "dev": true but remained a real risk in CI environments and misconfigured builds.
  • The fix is backward-compatible: js-yaml 4.3.1 and 3.15.1 parse all valid YAML documents identically to their predecessors; only the internal complexity of the omap resolver changed.

How Orbis AppSec Detected This

  • Source: Any code path passing untrusted or user-controlled YAML strings into js-yaml's load() or parse() functions — including configuration file readers, API endpoints accepting YAML payloads, and test harnesses using @istanbuljs/load-nyc-config.
  • Sink: The !!omap type resolver inside js-yaml's type system, invoked whenever a YAML document contains an !!omap tag — present in both the node_modules/js-yaml (v4.1.1) and node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml (v3.14.2) instances.
  • Missing control: No upper bound on !!omap entry count, and no O(1) data structure (such as a Set or Map) used for duplicate-key detection — allowing the nested loop to run to completion on arbitrarily large inputs.
  • CWE: CWE-407 — Inefficient Algorithmic Complexity
  • Fix: Upgraded js-yaml to 4.3.1 and removed the pinned vulnerable 3.14.2 sub-dependency from the @istanbuljs/load-nyc-config subtree in package-lock.json, replacing quadratic duplicate detection with a linear Set-based approach.

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 textbook example of how algorithmic complexity vulnerabilities hide in plain sight. The !!omap flaw in js-yaml wasn't a memory corruption bug or an injection vector — it was simply the wrong data structure for a duplicate-key check. But in a language like JavaScript, where a single-threaded event loop handles all I/O, even a few seconds of CPU saturation translates directly into a service outage.

What makes this case particularly instructive is the double exposure: the top-level dependency was vulnerable, and a nested transitive pin was also vulnerable — independently. Neither would have been caught by a developer who only reviewed package.json. This is why automated SCA scanning of lock files, not just manifest files, is non-negotiable in modern Node.js security practice.

Upgrade to js-yaml@4.3.1 or js-yaml@3.15.1, audit your lock files for nested pins, and let your tooling catch what your eyes will miss.


References

Frequently Asked Questions

What is quadratic CPU consumption in YAML parsing?

It is an algorithmic complexity flaw where processing time grows as O(n²) relative to input size, meaning a moderately large YAML document can consume CPU resources exponentially greater than a linear parser would.

How do you prevent algorithmic complexity vulnerabilities in JavaScript YAML parsing?

Pin js-yaml to a patched version (≥4.3.1 or ≥3.15.1), audit all transitive dependency pins in package-lock.json, and validate or size-limit YAML input before parsing.

What CWE is quadratic CPU consumption?

CWE-407 — Inefficient Algorithmic Complexity, which covers cases where an algorithm's time or space complexity is unnecessarily high relative to input size.

Is input validation alone enough to prevent this vulnerability in js-yaml?

Not reliably. While size-limiting input helps, the root cause is in the library's internal duplicate-key detection loop; only upgrading to the patched version fully eliminates the quadratic path.

Can static analysis detect this type of vulnerability?

Yes — software composition analysis (SCA) tools like Trivy, Snyk, and Dependabot flag known-vulnerable package versions in package-lock.json, which is exactly how this issue was identified.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #89

Related Articles

high

How Denial of Service via Unbounded Intermediate Arrays happens in JavaScript and how to fix it

CVE-2026-69152 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package (versions prior to 1.1.18/2.1.4/3.0.6/5.0.9) that allows attackers to crash a Node.js application by crafting glob patterns that generate unbounded intermediate arrays, effectively bypassing the earlier CVE-2026-14257 mitigation. The fix upgrades `brace-expansion` from 1.1.14 to 1.1.18 in `frontend/package-lock.json`, closing the bypass and restoring safe memory bounds during pattern expansion.

high

How Quadratic CPU Consumption happens in JavaScript YAML parsing and how to fix it

A high-severity denial-of-service vulnerability (GHSA-5p4m-2wfm-xmqj) was discovered in js-yaml affecting both the 3.x and 4.x branches, where parsing YAML documents containing `!!omap` tags triggers quadratic CPU consumption. The fix upgrades js-yaml from `^4.1.1` to `5.2.0` in the project's GitHub Actions workflow dependencies, closing the attack surface for any untrusted YAML input processed by CI/CD tooling.

critical

How Missing Rate Limiting happens in Express.js and how to fix it

Two public API endpoints in `server.js` — `/api/health` and `/api/contact` — were exposed without any rate limiting middleware, allowing attackers to exhaust server resources or spam an SMTP server with unlimited requests. The fix adds rate limiting to both endpoints, with stricter controls on the resource-intensive `/api/contact` route that triggers email sending operations. This change closes a directly exploitable denial-of-service vector in a production web service.

high

How Denial of Service via Specific Input Sequence happens in JavaScript (marked) and how to fix it

CVE-2026-41680 is a high-severity Denial of Service vulnerability in the marked Markdown parsing library, affecting versions prior to 18.0.2. By supplying a crafted input sequence to the parser, an attacker can cause the application to hang or exhaust resources, making the frontend unavailable. Upgrading marked from 18.0.0 to 18.0.2 in both `package.json` and `package-lock.json` closes the vulnerability without affecting valid Markdown rendering.

high

How Denial of Service via Unbounded Data Happens in JavaScript and how to fix it

CVE-2025-58754 is a high-severity Denial of Service vulnerability in the popular axios HTTP client library, caused by the absence of a data size check on incoming response or request payloads. An attacker who can influence the size of data processed by axios could exhaust server memory or CPU, bringing down dependent Node.js applications. The fix upgrades axios from version 1.8.4 to 1.18.0, closing the unbounded data processing path.

high

How Path Traversal happens in PostCSS Source Map Loading and how to fix it

A path traversal vulnerability in PostCSS versions before 8.5.18 allowed malicious `sourceMappingURL` comments in CSS files to trick PostCSS into loading arbitrary `.map` files from the filesystem. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `frontend/package-lock.json` and pins the version via an override in `frontend/package.json`, closing the file disclosure vector before it could be chained with other weaknesses.