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 Command Injection happens in Python PopClip Extensions and how to fix it

A critical command injection vulnerability was discovered in `contrib/Klipz.popclipext/Klipz.py`, where user-controlled clipboard content was concatenated directly into shell commands executed via `osascript`. The fix replaces unsafe string concatenation with `subprocess` and proper argument lists, and replaces the unsafe `pickle` serialization with `json` to eliminate a secondary deserialization risk. Together, these changes close two distinct attack surfaces in a single file.

high

How Unsafe Deserialization into interface{} happens in Go and how to fix it

A high-severity unsafe deserialization vulnerability was discovered in `web/session/session.go` where a type assertion on an `interface{}` value was performed without checking success, enabling arbitrary data structures to flow into the application. The fix adds a two-branch type assertion that returns `nil` when the cast fails, preventing unexpected types from propagating. This pattern is common in Go session management code and is easy to overlook during code review.

high

How trailofbits.python.pickles-in-pytorch.pickles-in-pytorch happens in Python/PyTorch and how to fix it

A high-severity deserialization vulnerability was fixed in `skills/packs/pipeline-phase-5-pretrain-code/scripts/trainer.py` where `torch.save()` was used to serialize model checkpoints. Because PyTorch's save mechanism relies on Python's `pickle` module internally, any checkpoint file loaded later could execute arbitrary code. The fix replaces `torch.save()` with `np.savez()` for model weights and a JSON file for metadata, eliminating the pickle-based serialization entirely.

high

How Denial of Service via Unbounded Brace Expansion Happens in Node.js Dependencies and How to Fix It

A critical vulnerability in adm-zip (CVE-2026-39244) allowed attackers to craft malicious ZIP files that trigger unbounded brace expansion, causing excessive memory allocation and process crashes. The CortexKit project fixed this by upgrading adm-zip from 0.5.17 to 0.6.0, which implements bounds checking on expansion operations. This vulnerability demonstrates why dependency management and timely security updates are essential for production Node.js applications.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SQL Injection happens in Node.js migration scripts and how to fix it

A high-severity SQL injection vulnerability was discovered in `scripts/setup-d1.mjs`, where migration filenames were directly concatenated into SQL INSERT statements using an inadequate `escapeSqlString` function. An attacker with filesystem write access could craft a malicious filename to execute arbitrary SQL commands against the Cloudflare D1 database. The fix replaces string concatenation with parameterized queries, eliminating the injection surface entirely.