Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"schema_version": "1.4.0",
"id": "GHSA-h67p-54hq-rp68",
"modified": "2026-06-15T17:15:07Z",
"modified": "2026-06-15T17:15:08Z",
"published": "2026-06-15T17:15:07Z",
"aliases": [
"CVE-2026-53550"
],
"summary": "JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases",
"details": "### Summary\nA crafted YAML document can trigger algorithmic CPU exhaustion in `js-yaml` merge-key processing (`<<`) by repeating the same alias many times in a merge sequence. \nThis causes quadratic parse-time behavior relative to input size and can block a Node.js worker/event loop for seconds with a relatively small payload (tens of KB), resulting in denial of service.\n\n### Details\nThe issue is in merge handling inside `lib/loader.js`:\n\n- `storeMappingPair(...)` iterates every element of a merge sequence when key tag is `tag:yaml.org,2002:merge`.\n- For each element, it calls `mergeMappings(...)`.\n- `mergeMappings(...)` computes `Object.keys(source)` and performs `_hasOwnProperty.call(destination, key)` checks for each key.\n\nWhen input is of the form:\n\na: &a {k0:0, k1:0, ..., kK:0}\nb: {<<: [*a, *a, *a, ... repeated M times ...]}\nall *a entries refer to the same anchored object. After the first merge, subsequent merges are semantically no-ops, but the parser still reprocesses all keys each time.\nResulting work is O(K * M), while input size is O(K + M), giving quadratic scaling as payload grows.\nRelevant code path:\nlib/loader.js in storeMappingPair(...) merge branch (keyTag === 'tag:yaml.org,2002:merge')\nlib/loader.js mergeMappings(...)\n\n\n### Root cause\nFile: lib/loader.js\nFunction: storeMappingPair(state, _result, overridableKeys, keyTag, keyNode,\n valueNode, startLine, startLineStart, startPos)\nLines: ~359-366\n\n if (keyTag === 'tag:yaml.org,2002:merge') {\n if (Array.isArray(valueNode)) {\n for (index = 0, quantity = valueNode.length; index < quantity; index += 1) {\n mergeMappings(state, _result, valueNode[index], overridableKeys);\n }\n } else {\n mergeMappings(state, _result, valueNode, overridableKeys);\n }\n }\n\nWhen the merge value is a sequence (YAML 1.1 <<: [ *a, *a, ... ]), each element\nis handed to mergeMappings() without deduplication. mergeMappings() then does\n\n sourceKeys = Object.keys(source);\n for (index = 0; index < sourceKeys.length; index += 1) {\n key = sourceKeys[index];\n if (!_hasOwnProperty.call(destination, key)) {\n setProperty(destination, key, source[key]);\n overridableKeys[key] = true;\n }\n }\n\nEvery alias reference in the sequence resolves (by design) to the SAME object\nvia state.anchorMap. After the first merge, every subsequent merge of that same\nreference is a pure no-op semantically, but still performs:\n\n * one Object.keys(source) call (O(K))\n * K _hasOwnProperty.call checks on the destination\n\nTotal: M * K hasOwnProperty checks + M Object.keys allocations, while the final\nobject and all observable side effects are identical to a single merge.\n\nYAML semantics for `<<:` are idempotent and commutative over duplicate sources,\nso collapsing duplicates preserves behavior exactly; this isn't a spec trade-off.\n\n\n### PoC\nEnvironment:\njs-yaml version: 4.1.1\nNode.js: v24.5.0\nPlatform: arm64 macOS (reproduced consistently)\nReproduction script:\nCreate many keys in one anchored map (&a).\nMerge that same alias repeatedly via <<: [*a, *a, ...].\nMeasure parse time and compare with control payload using single merge (<<: *a).\nObserved repeated runs (same machine):\nK=M=1000, input 9,909 bytes: ~33–36 ms\nK=M=2000, input 20,909 bytes: ~121–123 ms\nK=M=4000, input 42,909 bytes: ~524–537 ms\nK=M=6000, input 64,909 bytes: ~1,608–1,829 ms\nK=M=8000, input 86,909 bytes: ~3,395–3,565 ms\nControl (single merge, similar key counts):\nK=2000: ~1–2 ms\nK=4000: ~3 ms\nK=8000: ~5 ms\nAlso verified: repeated-merge output equals single-merge output (same key count and same JSON), confirming excess time is redundant computation.\n\n\n### Impact\nThis is a denial-of-service vulnerability (CPU exhaustion / algorithmic complexity).\nAny service parsing untrusted YAML with js-yaml can be impacted, including API backends, CI tools, config processors, and automation services. An attacker can submit crafted YAML to significantly increase CPU time and reduce availability.\n\n### Suggested fix:\nDedupe the merge source list by reference before invoking mergeMappings. Any of\nthe following are minimal and preserve YAML 1.1 merge semantics:\n\ndedupe in storeMappingPair:\n\n if (keyTag === 'tag:yaml.org,2002:merge') {\n if (Array.isArray(valueNode)) {\n var seen = new Set();\n for (index = 0, quantity = valueNode.length; index < quantity; index += 1) {\n var src = valueNode[index];\n if (seen.has(src)) continue; // idempotent; skip redundant alias\n seen.add(src);\n mergeMappings(state, _result, src, overridableKeys);\n }\n } else {\n mergeMappings(state, _result, valueNode, overridableKeys);\n }\n }",
"details": "### Summary\nA crafted YAML document can trigger algorithmic CPU exhaustion in `js-yaml` merge-key processing (`<<`) by using large merge sequences that repeatedly reference the same or structurally equivalent mappings. This can exhibit near-quadratic worst-case parse-time behavior relative to input size and block a Node.js worker/event loop for seconds with a relatively small payload (tens of KB), resulting in denial of service.\n\n### Details\nThe issue is in merge handling inside `lib/loader.js`:\n\n- `storeMappingPair(...)` iterates every element of a merge sequence when the key tag is `tag:yaml.org,2002:merge`.\n- For each element, it calls `mergeMappings(...)`.\n- `mergeMappings(...)` computes `Object.keys(source)` and performs `_hasOwnProperty.call(destination, key)` checks for each key.\n\nWhen input is of the form:\n\n```yaml\na: &a {k0: 0, k1: 0, ..., kK: 0}\nb: {<<: [*a, *a, *a, ... repeated M times ...]}\n```\n\nall `*a` entries refer to the same anchored object. After the first merge, subsequent merges are semantically no-ops, but the parser still reprocesses all keys each time. Resulting work is \\(O(K \\times M)\\) while input size is \\(O(K + M)\\), giving near-quadratic scaling as payload grows.\n\nRelevant code path:\n\n- `lib/loader.js` `storeMappingPair(...)` merge branch (`keyTag === 'tag:yaml.org,2002:merge'`)\n- `lib/loader.js` `mergeMappings(...)`\n\n```js\n// lib/loader.js (simplified)\n\nif (keyTag === 'tag:yaml.org,2002:merge') {\n if (Array.isArray(valueNode)) {\n for (index = 0, quantity = valueNode.length; index < quantity; index += 1) {\n mergeMappings(state, _result, valueNode[index], overridableKeys);\n }\n } else {\n mergeMappings(state, _result, valueNode, overridableKeys);\n }\n}\n\n// mergeMappings\nsourceKeys = Object.keys(source);\nfor (index = 0; index < sourceKeys.length; index += 1) {\n key = sourceKeys[index];\n if (!_hasOwnProperty.call(destination, key)) {\n setProperty(destination, key, source[key]);\n overridableKeys[key] = true;\n }\n}\n```\n\nEvery alias reference in the sequence resolves (by design) to the same object via the anchor map. After the first merge, every subsequent merge of that same reference is a pure no-op semantically, but still performs one `Object.keys(source)` call and `K` `_hasOwnProperty.call` checks on the destination. The final merged object is identical to the result of a single merge; only the CPU cost changes.\n\nAdditionally, an attacker can construct long merge chains using distinct anchors that resolve to different but structurally equivalent mappings. Each such source still incurs a full merge, which can drive near-quadratic behavior for large `K` and `M`.\n\n### Impact\nThis is a denial-of-service vulnerability via CPU exhaustion. Any service parsing untrusted YAML with `js-yaml` can be impacted, including API backends, CI tools, configuration processors, and automation services. An attacker can submit crafted YAML to significantly increase CPU time and reduce availability.\n\n### Suggested fix (library)\nDedupe the merge source list by reference before invoking `mergeMappings` so that repeated aliases do not trigger redundant work. This preserves YAML merge semantics in this context because duplicate merge sources are idempotent and commutative.\n\n#### Additional hardening\nTo prevent near-quadratic behavior even when an attacker uses many distinct but structurally similar sources, enforce a limit on the number of merge sources processed for a single mapping. This limit is an implementation safety boundary (not required by the YAML spec) and may cause very large or deeply merged documents to fail fast rather than parse slowly.\n\nOne possible implementation that combines deduplication with a hard ceiling is:\n\n```js\n// Example hardening: dedupe merge sources and cap unique sources per merge\nvar MERGE_SOURCE_LIMIT = 10000; // implementation limit; configurable safety valve\n\nif (keyTag === 'tag:yaml.org,2002:merge') {\n if (Array.isArray(valueNode)) {\n var seen = new Set();\n var mergeCount = 0;\n\n for (index = 0, quantity = valueNode.length; index < quantity; index += 1) {\n var src = valueNode[index];\n\n // Deduplicate identical aliases / sources by reference (spec-preserving).\n if (seen.has(src)) continue;\n seen.add(src);\n\n if (++mergeCount > MERGE_SOURCE_LIMIT) {\n throwError(state, 'merge source limit exceeded');\n }\n\n mergeMappings(state, _result, src, overridableKeys);\n }\n } else {\n mergeMappings(state, _result, valueNode, overridableKeys);\n }\n}\n```",
"severity": [
{
"type": "CVSS_V3",
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L"
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
}
],
"affected": [
Expand Down Expand Up @@ -46,13 +46,18 @@
{
"type": "PACKAGE",
"url": "https://github.com/nodeca/js-yaml"
},
{
"type": "WEB",
"url": "http://yaml.org/type/merge.html"
}
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-407"
],
"severity": "MODERATE",
"severity": "HIGH",
"github_reviewed": true,
"github_reviewed_at": "2026-06-15T17:15:07Z",
"nvd_published_at": null
Expand Down