fix(security): bound YAML alias expansion in file-parser (billion-laughs DoS)#5756
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview The YAML parser now runs
Adds Reviewed by Cursor Bugbot for commit 5c5d8aa. Configure here. |
Greptile SummaryThis PR limits YAML alias expansion before parsed documents are serialized. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (3): Last reviewed commit: "yaml guard: charge lone surrogates at th..." | Re-trigger Greptile |
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.
|
@cursor review |
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.
|
@cursor review |
There was a problem hiding this comment.
✅ 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.
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
Fix
Add a bounded, iterative traversal (`assertYamlWithinLimits`) that runs on the compact DAG before `JSON.stringify`, aborting once any of these caps is exceeded:
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`:
All 7 tests pass; typecheck and biome clean.