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
•Technically reviewed by Anupam Mediratta•Published August 9, 2026•Reviewed 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

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

critical

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.