Skip to content

fix(security): bound YAML alias expansion in file-parser (billion-laughs DoS)#5756

Merged
waleedlatif1 merged 3 commits into
stagingfrom
worktree-yaml-billion-laughs-fix
Jul 18, 2026
Merged

fix(security): bound YAML alias expansion in file-parser (billion-laughs DoS)#5756
waleedlatif1 merged 3 commits into
stagingfrom
worktree-yaml-billion-laughs-fix

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

The YAML file parser (`apps/sim/lib/file-parsers/yaml-parser.ts`) called `yaml.load()` then `JSON.stringify()` on the result. YAML aliases (`*anchor`) resolve to shared references, so `yaml.load` cheaply builds a compact in-memory DAG — but `JSON.stringify` does not preserve sharing and expands that DAG into a full tree, duplicating every shared node. A crafted sub-1 KB `.yaml`/`.yml` document therefore expands to hundreds of MB / GB during serialization, pinning CPU and forcing enormous transient allocations that OOM-kill or wedge the worker (CWE-776).

The existing 100 MB download / file-size caps don't help — the amplification happens after the size check, at `JSON.stringify` time.

Reachability

  • Workflow File parse block → `POST /api/files/parse` → `parseYAMLBuffer`.
  • Knowledge-base ingestion → `document-processor.ts` → `parseDocument` → YAML parser, running on a shared background worker serving other tenants. One tiny upload can OOM-kill the worker (co-tenant DoS).

Fix

Add a bounded, iterative traversal (`assertYamlWithinLimits`) that runs on the compact DAG before `JSON.stringify`, aborting once any of these caps is exceeded:

  • Expanded node count (5,000,000) — repeated aliased references are counted each time they're reached, mirroring how `JSON.stringify` expands them, so the bomb is detected against the small DAG. Also terminates cyclic anchor structures.
  • Estimated serialized size (64 MB) — accounts for the pretty-print indentation overhead that dominates deep alias bombs, so amplification is rejected before the bytes are ever allocated.
  • Nesting depth (500) — defense-in-depth (js-yaml itself already caps load depth at 100).

This also replaces the unbounded recursive `getYamlDepth` — which spread large arrays into `Math.max(...array)` (stack-overflow risk) — with the same single bounded walk that computes depth for metadata.

On breach a distinct `YamlComplexityError` is thrown (kept separate from the generic `Invalid YAML` syntax error so a resource-exhaustion attempt is distinguishable from a malformed file).

Tests

New `yaml-parser.test.ts`:

  • normal document + array metadata/depth still parse correctly
  • a <2 KB chained alias bomb is rejected as `YamlComplexityError` (whole suite runs in ~20 ms — proof the guard fires before any serialization)
  • malformed YAML still surfaces as `Invalid YAML`
  • direct guard coverage: depth-cap breach and cyclic structure both rejected via the node cap

All 7 tests pass; typecheck and biome clean.

The YAML parser called yaml.load() then JSON.stringify() on the result.
YAML aliases (*anchor) resolve to shared references, so yaml.load cheaply
builds a compact DAG, but JSON.stringify expands that DAG into a full tree,
duplicating every shared node. A crafted sub-1KB .yaml/.yml document could
therefore expand to hundreds of MB / GB during serialization, pinning CPU
and OOM-killing the shared parse/ingestion worker (CWE-776).

Add a bounded, iterative traversal that runs before JSON.stringify and
aborts once expanded node count, estimated serialized size, or nesting
depth exceeds safe caps. This detects the amplification against the compact
DAG before any allocation, and also terminates cyclic anchor structures.
Replaces the unbounded recursive getYamlDepth (which spread large arrays
into Math.max(...array), risking a stack overflow) with the same bounded
walk.
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 18, 2026 7:04pm

Request Review

@cursor

cursor Bot commented Jul 18, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Security-critical file ingestion path with new rejection behavior for oversized YAML; legitimate very large YAML near caps could be rejected, but normal documents are covered by tests.

Overview
Hardens YAML parsing against alias-expansion (“billion laughs”) DoS by validating the parsed document before JSON.stringify, since tiny inputs can explode when aliases are expanded at serialize time.

The YAML parser now runs assertYamlWithinLimits, an iterative walk that mirrors alias duplication with caps on expanded node count (5M), estimated pretty-printed JSON size (64 MB, including key and escape-aware string charging), and nesting depth (500). Breaches raise YamlComplexityError (distinct from syntax errors). The old unbounded recursive depth helper is removed in favor of this same bounded traversal.

POST /api/files/parse no longer falls back to returning the upload as raw UTF-8 when the YAML parser rejects a document for complexity—those errors propagate instead of bypassing the guard.

Adds yaml-parser.test.ts covering normal parses, alias/key bombs, malformed YAML, and direct limit behavior.

Reviewed by Cursor Bugbot for commit 5c5d8aa. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR limits YAML alias expansion before parsed documents are serialized. The main changes are:

  • Adds node-count, serialized-size, and nesting-depth limits.
  • Accounts for object keys, JSON escaping, and lone surrogates in size estimates.
  • Preserves complexity errors instead of treating them as ordinary syntax failures.
  • Adds tests for normal documents and several YAML expansion attacks.

Confidence Score: 5/5

This looks safe to merge.

  • The updated estimate includes repeated object keys and escaped string output.
  • Complexity limits run before JSON serialization.
  • Complexity errors remain distinct across the updated parser path.
  • No blocking issue was found in the changed code.

Important Files Changed

Filename Overview
apps/sim/lib/file-parsers/yaml-parser.ts Adds bounded traversal and detailed serialized-size accounting before YAML serialization.
apps/sim/lib/file-parsers/yaml-parser.test.ts Covers normal parsing, alias expansion, key amplification, escaping, cycles, and depth limits.
apps/sim/app/api/files/parse/route.ts Keeps YAML complexity rejections from falling back to generic parsing.

Reviews (3): Last reviewed commit: "yaml guard: charge lone surrogates at th..." | Re-trigger Greptile

Comment thread apps/sim/lib/file-parsers/yaml-parser.ts
Comment thread apps/sim/lib/file-parsers/yaml-parser.ts
Comment thread apps/sim/lib/file-parsers/yaml-parser.ts Outdated
Comment thread apps/sim/lib/file-parsers/yaml-parser.ts
Comment thread apps/sim/lib/file-parsers/yaml-parser.ts
Addresses review findings on the alias-expansion guard:

- Charge object keys, not just values. JSON.stringify re-emits every key on
  each alias expansion, so an aliased object with a long key amplified without
  being counted. Keys are now charged against the size cap.
- Compute the exact JSON-escaped string length (quotes, backslashes, control
  chars) instead of a flat multiplier. Plain text is charged its true 1:1 size
  (no false rejection of large legitimate documents) while escape-heavy strings
  are charged their real, larger cost (no cap bypass).
- Count each node as it is enqueued and only push container nodes onto the
  traversal stack. A pathologically wide fan-out now trips a cap during the
  enqueue loop instead of first materializing millions of stack entries and
  exhausting memory inside the guard itself.
- Fail closed in POST /api/files/parse: a YamlComplexityError rejection is now
  re-thrown (mirroring isPayloadSizeLimitError) rather than silently falling
  back to storing the crafted document as raw text.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/file-parsers/yaml-parser.ts
serializedStringLength treated surrogate code units as cost 1, but
well-formed JSON.stringify escapes a lone surrogate to \uXXXX (six units).
Charge lone surrogates as 6 and valid high+low pairs as-is (two units),
matching JSON.stringify exactly. Verified against JSON.stringify across
plain text, escapes, control chars, lone high/low surrogates, and valid
pairs.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5c5d8aa. Configure here.

@waleedlatif1
waleedlatif1 merged commit 1b06b6c into staging Jul 18, 2026
20 checks passed
@waleedlatif1
waleedlatif1 deleted the worktree-yaml-billion-laughs-fix branch July 18, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant