Back to Blog
high SEVERITY7 min read

How Quadratic CPU Consumption in js-yaml's !!omap Resolution Happens in Node.js and How to Fix It

A high-severity algorithmic complexity vulnerability (GHSA-5p4m-2wfm-xmqj) in js-yaml versions 3.x through 4.3.0 allows attackers to trigger quadratic CPU consumption through specially crafted `!!omap` YAML sequences. The fix upgrades js-yaml to 4.3.1 using a pnpm override in the `e2e/adapter/claude-code` package, ensuring all transitive dependencies also receive the patched version. This proactive patch eliminates an exploit primitive before it can be chained with other weaknesses.

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

Answer Summary

GHSA-5p4m-2wfm-xmqj is a high-severity algorithmic complexity vulnerability (CWE-407) in the js-yaml npm package (versions 3.x and 4.x through 4.3.0) where the `!!omap` type resolution handler uses an O(n²) duplicate-key check, enabling denial-of-service via crafted YAML input. The fix is to upgrade js-yaml to version 4.3.1 (or 3.15.1 for the 3.x branch), which replaces the quadratic loop with a linear-time lookup. In this project, a pnpm override in `package.json` ensures all transitive consumers of js-yaml are pinned to the safe version.

Vulnerability at a Glance

cweCWE-407 (Inefficient Algorithmic Complexity)
fixUpgrade js-yaml to 4.3.1 via pnpm override to replace quadratic duplicate-key check with linear-time approach
riskDenial of service via quadratic CPU consumption when parsing untrusted YAML
languageJavaScript (Node.js)
root causejs-yaml 4.3.0's !!omap handler checks for duplicate keys using nested O(n²) iteration
vulnerabilityAlgorithmic Complexity / ReDoS-style Denial of Service in !!omap resolution

Introduction

In the e2e/adapter/claude-code end-to-end test package, Orbis AppSec's Trivy scanner flagged a high-severity vulnerability lurking in the pnpm-lock.yaml dependency tree: js-yaml@4.3.0 was subject to GHSA-5p4m-2wfm-xmqj, a quadratic CPU consumption bug in the !!omap (ordered map) type resolution handler. While the vulnerability existed in a transitive dependency pulled in through the autoevals package (visible in the lockfile snapshot), it represented a real exploit primitive — a pattern that automated attack tooling could chain with other weaknesses to achieve denial of service against any service parsing YAML from untrusted sources.

The specific problem: js-yaml's !!omap resolver performs duplicate-key validation using a nested loop that runs in O(n²) time. An attacker who can feed crafted YAML to any code path that eventually calls yaml.load() with the default schema can lock up a Node.js event loop with a surprisingly small payload.

The Vulnerability Explained

What is !!omap and Why Does It Matter?

YAML's !!omap type represents an ordered mapping — essentially an array of single-key objects where duplicate keys are forbidden. When js-yaml encounters a !!omap tag, it must validate that no two entries share the same key. In versions up to 4.3.0, this validation looked conceptually like:

// Simplified representation of the vulnerable pattern in js-yaml <= 4.3.0
for (let i = 0; i < pairs.length; i++) {
  for (let j = i + 1; j < pairs.length; j++) {
    if (pairs[i][0] === pairs[j][0]) {
      throw new Error('duplicate key in !!omap');
    }
  }
}

This nested iteration is O(n²) — for an ordered map with 10,000 entries, the inner comparison runs approximately 50 million times. For 100,000 entries, it's roughly 5 billion comparisons. The CPU time grows quadratically with the number of keys, and because Node.js is single-threaded, this blocks the entire event loop.

The Attack Scenario

Consider the dependency chain visible in the lockfile diff. The autoevals package (pinned at a specific version in the project) depends on js-yaml:

# From pnpm-lock.yaml snapshots section (before fix)
snapshots:
  autoevals:
    dependencies:
      ajv: 8.20.0
      compute-cosine-similarity: 1.1.0
      js-levenshtein: 1.1.6
      js-yaml: 4.3.0        # <-- vulnerable version
      linear-sum-assignment: 1.0.9
      mustache: 4.2.0

If any code path in the e2e adapter or its dependencies parses YAML that could originate from external input — configuration files, API responses, test fixtures loaded from repositories, or webhook payloads — an attacker could craft a YAML document like:

--- !!omap
- key_00001: value
- key_00002: value
- key_00003: value
# ... thousands more unique keys ...
- key_99999: value
- key_100000: value

Even though all keys are unique (so the validation ultimately passes), the O(n²) check must compare every pair before confirming there are no duplicates. A 100KB YAML file with ~10,000 omap entries could freeze a Node.js process for seconds; a 1MB file could hang it for minutes.

Why "Not Confirmed Reachable" Still Matters

The PR assessment notes the vulnerability is "present in dependency tree, not confirmed reachable." This is an honest assessment — the e2e test suite may not directly parse untrusted YAML through autoevals. However, this matters for three reasons:

  1. Dependency drift: Future code changes might introduce a path where untrusted YAML reaches js-yaml.
  2. Supply chain risk: If autoevals itself processes YAML from external sources, the vulnerability is reachable through the library.
  3. Exploit primitive removal: Automated exploit-development tools increasingly scan for known-vulnerable dependency versions and attempt to construct exploit chains. Removing the primitive raises the bar.

The Fix

The fix is surgical and elegant: a pnpm override in package.json forces every copy of js-yaml in the dependency tree to version 4.3.1, regardless of what transitive dependencies request.

Change 1: e2e/adapter/claude-code/package.json

Before:

{
  "devDependencies": {
    "@types/node": "^24.0.0",
    "typescript": "^5.7.2",
    "vitest": "^4.1.11"
  }
}

After:

{
  "devDependencies": {
    "@types/node": "^24.0.0",
    "typescript": "^5.7.2",
    "vitest": "^4.1.11"
  },
  "pnpm": {
    "overrides": {
      "js-yaml": "4.3.1"
    }
  }
}

The pnpm.overrides field is the pnpm equivalent of npm's overrides or Yarn's resolutions. It tells the package manager: "No matter what version any dependency requests for js-yaml, install 4.3.1 instead." This is critical because the project doesn't directly depend on js-yaml — it comes in transitively through autoevals.

Change 2: e2e/adapter/claude-code/pnpm-lock.yaml

The lockfile reflects the override taking effect:

# Before
overrides: {}  # (implicit)

# After
overrides:
  js-yaml: 4.3.1

And in the resolved snapshots:

# Before
js-yaml@4.3.0:
  resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}

# After
js-yaml@4.3.1:
  resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}

The integrity hash change confirms a genuinely different package is being installed. In the snapshots section, autoevals now resolves to the patched version:

# Before
autoevals:
  dependencies:
    js-yaml: 4.3.0

# After
autoevals:
  dependencies:
    js-yaml: 4.3.1

What js-yaml 4.3.1 Changes Internally

Version 4.3.1 replaces the nested-loop duplicate-key check in the !!omap resolver with a Set-based or hash-based lookup, reducing the complexity from O(n²) to O(n). The same 100,000-key omap that would freeze a process for minutes now resolves in milliseconds.

Prevention & Best Practices

1. Use Dependency Override Mechanisms

When a vulnerability exists in a transitive dependency you don't control directly, use your package manager's override feature:

Package Manager Mechanism Field in package.json
pnpm pnpm.overrides "pnpm": { "overrides": { "pkg": "version" } }
npm overrides "overrides": { "pkg": "version" }
Yarn resolutions "resolutions": { "pkg": "version" }

2. Treat YAML Parsing as a Security Boundary

If your application parses YAML from any external source:
- Use yaml.load() with the JSON_SCHEMA or FAILSAFE_SCHEMA to disable type coercion including !!omap
- Set resource limits (timeouts, input size caps) around parsing operations
- Consider using JSON.parse() instead if your data doesn't require YAML-specific features

3. Automate Dependency Scanning

Tools like Trivy, Snyk, and Dependabot catch known vulnerabilities in lockfiles. Integrate them into CI/CD pipelines so that PRs with vulnerable dependencies are flagged before merge.

4. Audit Transitive Dependencies

Run pnpm why js-yaml (or npm explain js-yaml) regularly to understand why a package is in your tree and whether you can upgrade or replace the parent dependency.

Key Takeaways

  • js-yaml's !!omap handler in versions ≤4.3.0 uses O(n²) duplicate-key validation, making it trivially exploitable for CPU-based denial of service with crafted YAML input containing thousands of ordered map entries.
  • Transitive dependencies are attack surface too: the vulnerable js-yaml@4.3.0 wasn't a direct dependency of e2e/adapter/claude-code — it came in through autoevals, making it invisible without lockfile scanning.
  • pnpm overrides are the correct tool for forcing a patched version across all transitive consumers when you can't wait for upstream to bump their dependency.
  • "Not confirmed reachable" doesn't mean safe: exploit primitives in the dependency tree should be removed proactively, especially as automated exploit-development tools grow more capable of chaining weaknesses.
  • The fix changed only 2 files (package.json and pnpm-lock.yaml) with zero functional impact on valid YAML inputs — a minimal, low-risk patch with high security value.

How Orbis AppSec Detected This

  • Source: The js-yaml package resolved in e2e/adapter/claude-code/pnpm-lock.yaml as a transitive dependency of autoevals, potentially exposed to YAML input from test fixtures, configuration files, or external data sources.
  • Sink: The !!omap type resolver inside js-yaml@4.3.0's load() function, which executes a quadratic duplicate-key validation loop on any YAML document containing ordered map sequences.
  • Missing control: No version pinning or override existed to enforce a patched js-yaml version across the transitive dependency tree; the lockfile resolved to the vulnerable 4.3.0 release.
  • CWE: CWE-407 (Inefficient Algorithmic Complexity)
  • Fix: Added a pnpm.overrides entry in package.json to force js-yaml@4.3.1 across all dependencies, and regenerated the lockfile to reflect the patched resolution.

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

The js-yaml !!omap quadratic complexity vulnerability (GHSA-5p4m-2wfm-xmqj) is a textbook example of how algorithmic inefficiency becomes a security issue. A simple nested loop for duplicate-key detection — code that works correctly for small inputs — becomes a denial-of-service vector when exposed to adversarial input. The fix was straightforward: upgrade to 4.3.1 where the check runs in linear time, and use pnpm overrides to ensure the patch reaches every consumer in the dependency tree.

For developers maintaining Node.js projects: audit your lockfiles regularly, understand your transitive dependency tree, and treat YAML parsing with the same caution you'd give to any input deserialization boundary. The next vulnerability in your dependency tree might not be "confirmed reachable" today — but tomorrow's code change or tomorrow's automated attack tool might close that gap.

References

Frequently Asked Questions

What is algorithmic complexity denial of service in YAML parsing?

It occurs when a YAML parser's internal algorithm (such as duplicate-key detection in ordered maps) uses inefficient nested loops, allowing an attacker to craft input that causes CPU time to grow quadratically with input size, effectively freezing the application.

How do you prevent algorithmic complexity DoS in Node.js YAML parsing?

Keep js-yaml updated to patched versions (4.3.1+ or 3.15.1+), use pnpm/npm overrides to enforce versions across transitive dependencies, avoid parsing untrusted YAML with the full schema (especially `!!omap`), and set parsing timeouts for user-supplied input.

What CWE is algorithmic complexity denial of service?

CWE-407 (Inefficient Algorithmic Complexity), which describes algorithms that consume disproportionate resources relative to input size, enabling denial of service.

Is upgrading js-yaml alone enough to prevent this vulnerability?

Upgrading the direct dependency is necessary but not sufficient — you must also ensure transitive dependencies use the patched version. Tools like pnpm overrides, npm overrides, or yarn resolutions force all copies in the dependency tree to the safe version.

Can static analysis detect algorithmic complexity vulnerabilities like this?

Static analysis tools like Trivy and Snyk can detect known vulnerable package versions via advisory databases (like GHSA). However, detecting novel algorithmic complexity issues in custom code typically requires specialized analysis or manual review of loop structures.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #192

Related Articles

critical

How Arbitrary Code Execution Happens in protobufjs and How to Fix It

CVE-2026-41242 is a critical vulnerability in protobufjs versions 8.0.0 and earlier that allows attackers to execute arbitrary code by injecting malicious type fields into protobuf definitions. The fix upgrades the dependency from `^8.0.0` to `^8.6.6` in `core/package.json`, eliminating the unsafe code path that processed attacker-controlled type metadata without validation.

high

How Quadratic CPU Consumption Happens in JS-YAML and How to Fix It

A critical vulnerability in JS-YAML versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through maliciously crafted YAML input using the `!!omap` tag resolver. The vulnerability stems from inefficient array operations in the ordered map resolution logic, which could be exploited for denial-of-service attacks. Upgrading to JS-YAML 4.3.1 or 3.15.1 patches this attack surface by optimizing the computational complexity of ordered map processing.

critical

How Type Confusion Vulnerabilities Happen in JavaScript Dependencies and How to Fix Them

A critical type confusion vulnerability (CVE-2021-23436) was discovered in immer 9.0.7, a popular immutable state management library used in the client application. By upgrading to immer 9.0.6, the vulnerability was patched, eliminating a flaw that could have allowed attackers to bypass previous security fixes (CVE-2020-28477). This fix demonstrates why keeping dependencies current is essential for maintaining application security.

critical

How Prototype Pollution Happens in i18next-fs-backend and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-48713) was discovered in i18next-fs-backend versions prior to 2.6.6, where specially crafted missing-key strings could pollute the JavaScript object prototype. This fix upgrades the dependency to patch the vulnerability and prevent attackers from injecting malicious properties into application objects.

high

How Quadratic CPU Consumption in js-yaml's !!omap Resolution Happens in Node.js and How to Fix It

A high-severity algorithmic complexity vulnerability (GHSA-5p4m-2wfm-xmqj) in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through crafted YAML input using the `!!omap` tag. The fix upgrades js-yaml from 4.1.1 to 4.3.1 in the Audex desktop music player, eliminating a denial-of-service vector that could freeze the Electron application when parsing untrusted YAML content.

critical

How Prototype Pollution Happens in JavaScript Carousel Libraries and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-27212) was discovered in Swiper versions up to 11.2.10, a popular JavaScript carousel library used in production web applications. This vulnerability could allow attackers to manipulate application behavior through the prototype chain. The fix involved upgrading Swiper from 11.2.10 to 12.1.2, which patches the underlying prototype pollution flaw.