Skip to content

fix: count empty merge sources against maxTotalMergeKeys - #797

Merged
puzrin merged 1 commit into
nodeca:masterfrom
spokodev:fix-empty-merge-source-dos
Aug 26, 2026
Merged

puzrin merged 1 commit into
nodeca:masterfrom
spokodev:fix-empty-merge-source-dos

Conversation

@spokodev

Copy link
Copy Markdown
Contributor

The maxTotalMergeKeys guard added for the quadratic merge-key DoS (GHSA-g796-fgmg-93mv, fixed in 5.2.0) charges the budget once per folded key. A merge source that folds no keys — an empty mapping — costs real work to process but never increments the counter, so the guard can be bypassed:

arr: &arr [{}, {}, {}, ...]   # N empty mappings
targets:
  - <<: *arr                  # repeated K times

mergeSource iterates every element of the <<: sequence, and aliasing one N-element array of empty maps into K targets does O(K*N) work — none of it counted.

Impact

With merge enabled (YAML11_SCHEMA, or CORE_SCHEMA.withTags(mergeTag) — the standard YAML-1.1 / v4-compat configuration), the load time scales quadratically:

payload time
N=K=800 (~13 KB) ~20 ms
N=K=3200 (~50 KB) ~180 ms
N=K=20000 (~500 KB) ~13 s

A ~1 MB document projects to minutes of CPU on a single core. This restores the same availability DoS the maxTotalMergeKeys guard was shipped to prevent. A real key-folding merge of comparable size is correctly rejected today (merge keys exceeded maxTotalMergeKeys), so the guard works for the intended shape but is defeated by zero-key sources.

Attacker → control: attacker supplies an untrusted YAML document to an app that loads with merge enabled; the defeated control is maxTotalMergeKeys (default 10000).

Fix

Track whether a source folded any keys; if it folded none, charge one unit for it. A sequence of empty mappings is then bounded by maxTotalMergeKeys like any other merge.

Tests

A case in test/core/pathological.test.mjs (next to the existing merge-chain test) asserts the empty-source payload is rejected; it hangs for ~13 s and loads on the current code, and throws promptly with the fix. Normal and single empty-map merges still load correctly. Full suite green.

The maxTotalMergeKeys guard (added for the quadratic merge-key DoS,
GHSA-g796-fgmg-93mv) charges the budget once per *folded* key. A merge
source that folds no keys — an empty mapping — therefore costs real work
but never touches the counter. Aliasing one N-element sequence of empty
mappings into K merge targets does O(K*N) work while the guard never
fires:

  arr: &arr [{}, {}, ...]   # N empty maps
  targets:
    - <<: *arr              # x K

With merge enabled (YAML11_SCHEMA), an ~500 KB document of this shape
takes ~13s to load and scales quadratically, restoring the DoS the guard
was meant to prevent; a real key-folding merge of the same size is
correctly rejected.

Charge one unit for a source that folds no keys, so a sequence of empty
mappings is bounded by maxTotalMergeKeys like any other merge. Normal and
single empty-map merges are unaffected.
@puzrin
puzrin merged commit 6a8e05f into nodeca:master Aug 26, 2026
1 check passed
@puzrin

puzrin commented Aug 26, 2026

Copy link
Copy Markdown
Member

Note: next time, things will be simpler if you report such a problem via the standard way instead of a PR https://github.com/nodeca/js-yaml?tab=security-ov-file.

The cost of writing code is much lower than that of PR processing. Especially when a PR needs to be reviewed urgently and there's no option to request improvements.

puzrin added a commit that referenced this pull request Aug 26, 2026
@puzrin

puzrin commented Aug 26, 2026

Copy link
Copy Markdown
Member

See dfd3a29

@openshift-merge-robot

This comment has been minimized.

@openshift-merge-robot

This comment was marked as off-topic.

opendroid pushed a commit to opendroid/the-infinity that referenced this pull request Sep 9, 2026
Security fix: empty mappings in merge sequences now count toward
maxTotalMergeKeys, and merge sequence size is hard-limited to 100,
bounding CPU usage (nodeca/js-yaml#797).
MatiasFernandez added a commit to GemTalk/Jasper that referenced this pull request Sep 9, 2026
The dependencyVulnFloors test asserted two things about checked-in JSON:
that package.json still declares the `fast-uri` and `js-yaml` overrides,
and that every copy the lockfile resolves sits at or above a hardcoded
floor. Neither earns its keep.

The first is a change detector — it only fails when someone deliberately
edits the line, and then tells them they edited the line they just
edited. The second freezes an advisory snapshot from the day it was
written. A floor is a ratchet with a manual crank, and nothing turns it:
when the world moves past the floor the test does not go red, it goes
vacuously green, which is the worst failure mode a guard can have. That
already happened — `fast-uri`'s override moved to ^3.1.7 in #544 while
its floor stayed at 3.1.4, and #582 moves `js-yaml` to ^4.3.2 (the
backport of the merge-key limits, nodeca/js-yaml#797) against a floor
still reading 4.3.0, so the check would now accept a refresh back to a
version with a known unpatched CPU-DoS. Keeping it honest means
remembering to edit a constant on every Dependabot bump, and two bumps
in, we already did not.

It was also mis-housed: a repo-policy check over package.json and
package-lock.json, sitting in the client vitest suite that wants a live
stone, when its actual neighbours are `lint:lockfile` and
`lint:supply-chain`.

Nothing replaces it for now. Dependabot alerts already watch the same
advisory database with data that stays current, and they arrive as PRs
we can merge rather than as a constant someone has to maintain by hand.
Knapp-Kevin pushed a commit to MythologIQ-Labs-LLC/FailSafe that referenced this pull request Sep 10, 2026
…456)

Security-motivated patch bump: hard-limits YAML merge-sequence size and counts empty mappings toward maxTotalMergeKeys, fixing a CPU-exhaustion DoS vector (nodeca/js-yaml#797). Lockfile-only diff, no protected surface touched. 8/9 required checks green (CodeQL neutral/informational), mergeable_state clean at base main c369cee.
thomas-hochbichler added a commit to thomas-hochbichler/obsidian-remarkable-tagged-sync that referenced this pull request Sep 10, 2026
A dev-only transitive dependency, and the same one-line lockfile change
Dependabot proposed in #140. 4.3.2 backports two limits on YAML merge keys from
v5.4.1: a hard cap of 100 on a merge sequence, and empty mappings counted
toward maxTotalMergeKeys so they cannot be used to burn CPU (nodeca/js-yaml#797).

Reproduced by hand rather than merged from #140 because the Git identity gate
checks every commit a branch adds to main against one address, and a Dependabot
commit can never carry it. That makes every Dependabot PR structurally red here,
which is worth deciding about separately -- this commit only takes the bump.

`npm run matrix` reads the test matrix through js-yaml and still passes; the
full suite is green.


Claude-Session: https://claude.ai/code/session_0134WoHk57opQQAtELDmPorU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
SeriousJul added a commit to SeriousJul/seriousjul.github.io that referenced this pull request Sep 25, 2026
Re-pin the transitive dependency js-yaml from 4.1.1 to 4.3.2 in
package-lock.json (one node_modules/js-yaml block). package.json is untouched:
all five dependents require ^4.1.0, which 4.3.2 satisfies, so no override and no
range rewrite is needed.

CVE-2026-84375 (GHSA-2883-xcg3-v3hh, high, CVSS 7.5): maxTotalMergeKeys charged
one budget unit per folded key, so a merge source that folds no keys, an empty
mapping {}, cost real work but added nothing to the counter. One aliased sequence
of N empty mappings merged into K targets did O(N * K) work while the counter
never moved, so no configured limit could stop it. 4.3.2 charges one unit per
merge source on top of one unit per key (nodeca/js-yaml#797). On 4.1.1 there is
no limit to bypass and none to configure, so the dependency must move.

4.3.2 also clears the three older js-yaml alerts, because 4.1.1 sits inside the
vulnerable range of each: alert 29 (GHSA-5p4m-2wfm-xmqj), alert 16
(CVE-2026-59869 / GHSA-52cp-r559-cp3m), and alert 12 (CVE-2026-53550 /
GHSA-h67p-54hq-rp68).

js-yaml is build-time only and never reaches the browser bundle. It parses
repo-authored front matter and config, and it also runs on fork pull requests in
CI, which is the untrusted input path this pin closes.

Adds tests/js-yaml-security.test.mjs: the exact 4.3.2 cost model, the no-option
default budget, the real @11ty/gray-matter front-matter engine, unchanged merge
precedence, and a parse of every front matter block and YAML file tracked here.

API compatible: the same 14 exports, and the load and dump calls the dependents
use did not change.

Refs dependabot alert 48. The alert is fixed, not resolved; resolving it stays a
maintainer decision.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
Merged as-is — single-file lockfile bump in `/electron`, 3 lines, transitive resolution only.

js-yaml 4.3.2 hard-limits the merge-sequence size and counts empty mappings toward `maxTotalMergeKeys` (nodeca/js-yaml#797) — CPU-DoS hardening, which is worth taking promptly and carries no API surface change.

Verified against the tip before merging: `electron/package-lock.json` is the only file touched.
This was referenced Sep 29, 2026
lbruton added a commit to lbruton/StakTrakr that referenced this pull request Oct 10, 2026
…1569)

## Summary

Re-targets the Dependabot security update from #1565 onto `dev`.
Dependabot security updates ignore `target-branch: "dev"` in
`.github/dependabot.yml` and always open against `main`; merging there
would add `main`-only lockfile drift ahead of the next ship.

- `brace-expansion` 1.1.18 → 1.1.21 (transitive via `eslint` →
`minimatch`)
- `js-yaml` 4.3.1 → 4.3.2 (transitive via `@eslint/eslintrc`); backports
merge-key CPU limits (nodeca/js-yaml#797)

Lockfile-only, identical diff to #1565 (applied cleanly to `dev`). Both
are dev dependencies; no runtime code touched.

## Verification

- `npm ci` clean; `npm ls` confirms `brace-expansion@1.1.21`,
`js-yaml@4.3.2`
- `npm run lint` clean
- `npm run test:unit` — 1047 pass, 0 fail
- `npm audit`: these two advisories cleared; one pre-existing moderate
(`@humanfs/node`) remains, out of scope

Test inventory delta: +0 -0 tests, +0 -0 files.

Supersedes #1565 (closed).
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.

3 participants