Back to Blog
high SEVERITY8 min read

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) in js-yaml versions 4.3.0 and 3.x caused quadratic CPU consumption when resolving `!!omap` (ordered map) types in YAML documents. Attackers who could supply crafted YAML input could cause CPU exhaustion proportional to the square of the input size, potentially grinding Node.js services to a halt. The fix upgrades js-yaml to 4.3.1 and pins the version via a `package.json` overrides block to ensure no transitive dependency can r

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

Answer Summary

GHSA-5p4m-2wfm-xmqj is a high-severity algorithmic complexity vulnerability (CWE-407) in the JavaScript library js-yaml, affecting versions 4.3.0 and 3.x. When parsing YAML documents containing the `!!omap` (ordered map) tag, the library's resolution logic performed a duplicate-key check using a nested loop, resulting in O(n²) CPU time relative to the number of entries. This means an attacker who can supply crafted YAML input can cause exponential CPU exhaustion in any Node.js application that parses untrusted YAML. The fix upgrades js-yaml to 4.3.1 in `package-lock.json` and adds a `"overrides"` block in `package.json` to pin the safe version across the entire dependency tree.

Vulnerability at a Glance

cweCWE-407 (Inefficient Algorithmic Complexity)
fixUpgrade js-yaml from 4.3.0 to 4.3.1 and add a package.json overrides block to pin the safe version
riskCPU exhaustion leading to denial of service in any service parsing untrusted YAML
languageJavaScript / Node.js
root causejs-yaml's !!omap type resolver performed an O(n²) duplicate-key check during ordered map construction
vulnerabilityAlgorithmic Complexity / Quadratic CPU Consumption (ReDoS-class DoS)

The Hidden Cost of Parsing Ordered Maps: js-yaml's Quadratic CPU Bug

When your Node.js service parses a YAML configuration file or API payload, you probably assume the operation takes time proportional to the size of the document — parse 100 keys, spend roughly 100 units of work. But a subtle flaw in js-yaml's handling of the !!omap (ordered map) type tag broke that assumption entirely. A crafted YAML document with enough ordered-map entries could force the parser to spend quadratic time — 10,000 units of work for 100 entries, 1,000,000 units for 1,000 entries — turning a routine parse call into a CPU-exhausting denial-of-service vector.

This post breaks down exactly what went wrong, how the fix works, and what you can do to protect your own applications.


The Vulnerability Explained

What is !!omap?

YAML supports a rich set of type tags. The !!omap tag represents an ordered mapping — a sequence of key-value pairs where insertion order is preserved and duplicate keys are forbidden. A valid !!omap document looks like this:

!!omap
- alpha: 1
- beta: 2
- gamma: 3

The js-yaml library must validate this structure: it needs to confirm that no key appears more than once. That duplicate-detection logic is where the vulnerability lives.

The O(n²) Duplicate-Key Check

In js-yaml 4.3.0 (and the 3.x line prior to the fix), the !!omap type resolver iterated over every entry in the ordered map and, for each entry, scanned all previously seen entries to check for a duplicate key. In pseudocode:

// Vulnerable pattern (conceptual — pre-fix behaviour)
for (let i = 0; i < pairs.length; i++) {
  for (let j = 0; j < i; j++) {
    if (pairs[j].key === pairs[i].key) {
      throw new Error('duplicate key');
    }
  }
}

This is a classic O(n²) nested loop. For a document with n key-value pairs:

Entries (n) Comparisons (n²)
100 10,000
1,000 1,000,000
10,000 100,000,000
100,000 10,000,000,000

An attacker who can submit YAML input to your application — via a configuration endpoint, a file upload, a webhook payload, or any other surface — can craft a single !!omap document with tens of thousands of unique keys and peg one CPU core at 100% for seconds or minutes per request.

A Concrete Attack Scenario

Imagine a Node.js service that accepts YAML-formatted rule definitions from authenticated users (a CI/CD platform, a network policy editor, an infrastructure-as-code tool). The route handler calls:

const yaml = require('js-yaml');
app.post('/rules', (req, res) => {
  const rules = yaml.load(req.body.yaml); // ← vulnerable in js-yaml 4.3.0
  applyRules(rules);
  res.json({ ok: true });
});

A malicious user submits:

!!omap
- key_0000001: value
- key_0000002: value
# ... 50,000 more unique keys ...
- key_0050000: value

The yaml.load() call triggers the quadratic duplicate-check loop. With 50,000 entries, the resolver performs ~1.25 billion comparisons. On a modern server this can take 30–60 seconds of pure CPU time — per request. Even a handful of concurrent requests can saturate all available CPU cores, denying service to legitimate users.

Because the vulnerability is in the parsing stage, no application-level business logic needs to be reached. The damage is done before applyRules() is ever called.


The Fix

What Changed in package-lock.json

The pull request makes a targeted, two-file change. In package-lock.json, the pinned version of node_modules/js-yaml moves from the vulnerable 4.3.0 to the patched 4.3.1:

 "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==",

The integrity hash change confirms this is a genuinely different package artifact, not just a metadata update.

Why the package.json Overrides Block Matters

Updating package-lock.json alone protects the direct dependency, but js-yaml is a popular library that frequently appears as a transitive dependency — pulled in by tools like webpack, jest, eslint, or dozens of other packages. Without an explicit override, npm install could silently resolve a transitive path back to 4.3.0.

The fix adds an overrides block to package.json to prevent this:

+  "overrides": {
+    "js-yaml": "4.3.1"
+  }

This npm v8+ feature tells the package manager: regardless of what any dependency requests, always resolve js-yaml to 4.3.1. It is a belt-and-suspenders measure that makes the fix durable across future npm install runs and dependency tree changes.

How 4.3.1 Fixes the Algorithm

js-yaml 4.3.1 replaces the nested-loop duplicate check with a Set-based lookup, which has O(1) average-case insertion and membership testing:

// Fixed pattern (post-4.3.1 behaviour)
const seenKeys = new Set();
for (const pair of pairs) {
  if (seenKeys.has(pair.key)) {
    throw new Error('duplicate key');
  }
  seenKeys.add(pair.key);
}

The total work is now O(n) — linear in the number of entries. The same 50,000-entry !!omap document that previously triggered billions of comparisons now requires exactly 50,000 Set operations. The attack surface collapses entirely.


Key Takeaways

  • !!omap is a legitimate YAML attack surface: Any application that parses YAML from untrusted sources and uses js-yaml 4.3.0 or 3.x is exposed to CPU exhaustion via crafted ordered-map documents.
  • The package-lock.json version alone is not enough: Without the "overrides" block in package.json, future npm install runs can silently reintroduce the vulnerable 4.3.0 through transitive dependencies.
  • O(n²) bugs are invisible at test scale: The duplicate-key loop in js-yaml's !!omap resolver looked correct and passed all unit tests — the problem only manifests with large inputs designed to trigger worst-case behaviour.
  • Set-based lookups are the right fix for uniqueness checks: Replacing the nested array scan with a Set drops the complexity from O(n²) to O(n) and eliminates the attack entirely.
  • Integrity hashes in package-lock.json are your tamper-evidence: The SHA-512 change from sha512-1td788... to sha512-CY6crG... confirms you are running genuinely different (patched) code, not just a metadata change.

How Orbis AppSec Detected This

  • Source: Untrusted YAML content supplied to yaml.load() calls anywhere in the application or its dependency chain that processes external input.
  • Sink: The !!omap type resolver inside node_modules/js-yaml (version 4.3.0 as recorded in package-lock.json), which performed a quadratic duplicate-key scan during ordered-map construction.
  • Missing control: No algorithmic complexity guard existed in the !!omap resolver; the library used a nested array iteration instead of a constant-time Set lookup, and no input-size limit was enforced upstream.
  • CWE: CWE-407 — Inefficient Algorithmic Complexity
  • Fix: package-lock.json was updated to resolve node_modules/js-yaml to version 4.3.1, and a "overrides": { "js-yaml": "4.3.1" } block was added to package.json to pin the patched version across all transitive dependency paths.

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 denial-of-service vulnerabilities don't always look dramatic. There is no memory corruption, no remote code execution, no leaked secrets — just a nested loop that grows quadratically and a YAML tag that most developers have never typed. Yet the impact is real: a single HTTP request carrying a crafted !!omap document can saturate a CPU core for tens of seconds, and a handful of concurrent requests can take down a service entirely.

The fix is surgical: two files changed, one version bumped, one overrides entry added. The patched algorithm is O(n) instead of O(n²), and the overrides block ensures the fix survives future dependency tree changes. If your project depends on js-yaml — directly or transitively — verify you are on 4.3.1 (or 3.15.1 for the 3.x line) today.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #22

Related Articles

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.