LearnNewsExamplesServices
Frontmatter
id17461
titleDependabot 221: gray-matter pins js-yaml to a vulnerable 3.x line
stateClosed
labels
enhancementaibuild
assigneesneo-opus-ada
createdAtAug 21, 2026, 2:35 PM
updatedAtAug 21, 2026, 6:07 PM
githubUrlhttps://github.com/neomjs/neo/issues/17461
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 21, 2026, 6:07 PM

Dependabot 221: gray-matter pins js-yaml to a vulnerable 3.x line

Closed Backlog/active-chunk-18 enhancementaibuild
neo-opus-ada
neo-opus-ada commented on Aug 21, 2026, 2:35 PM

Context

Dependabot alert 221 (GHSA-5p4m-2wfm-xmqj, high, dev-scope): JS-YAML quadratic CPU consumption in !!omap resolution. Raised by @tobiu, who asked the obvious question first — "can we upgrade to the latest version?" — and the answer is no, which is why this needs a ticket rather than a version bump.

The Problem

We are already on the safe major, and it does not help. package.json declares js-yaml: ^5.2.0, resolved to 5.2.3 — not affected. The vulnerable copy is a second, nested install:

neo.mjs@13.1.0
+-- js-yaml@5.2.3          <- ours, safe
+-- gray-matter@4.0.3
|   `-- js-yaml@3.15.0     <- vulnerable

gray-matter pins js-yaml: ^3.13.1, so npm update will never move it off the 3.x line.

Three upgrade paths that do not work, checked rather than assumed:

  1. Upgrade js-yaml — already latest-major here; the direct dep is not the vulnerable one.
  2. Upgrade gray-matter4.0.3 IS the latest published version. It is unmaintained; there is nothing to move to.
  3. Override js-yaml to 5.xbreaks gray-matter at runtime. lib/engines.js:16-17 binds yaml.safeLoad / yaml.safeDump, both removed after 3.x. This would trade a CPU-exhaustion alert for a hard TypeError on every front-matter read.

The Architectural Reality

The advisory metadata and the published 3.x line disagree, and the code settles it. The advisory summary says "3.x and 4.x — CVE-2026-59870 fix not backported", while Dependabot reports first_patched_version: 3.15.1. Unpacking js-yaml@3.15.1 shows the backport is real:

// 3.15.0 (vulnerable) — linear scan inside the pair loop
if (objectKeys.indexOf(pairKey) !== -1) return false;

// 3.15.1 (fixed) — O(1) property probe
if (_hasOwnProperty.call(objectKeys, pairKey)) return false;
Object.defineProperty(objectKeys, pairKey, { value: true });

Different mechanism from 5.x's Set, same quadratic elimination. And 3.15.1 still exports safeLoad/safeDump (lib/js-yaml.js:24,27), verified by parsing with it — so gray-matter keeps working.

Exploitability here is currently nil, and that is a measurement, not a shrug. gray-matter only YAML-parses front matter. Two files in resources/content/** do contain !!omappulls/chunk-10/pr-16608.md and pulls/chunk-5/pr-15627.md — but in both the occurrences sit at lines 85/93 and 28/32, after front-matter ends at 19 and 17. They are prose in the body, never parsed. The input is also our own generated corpus, not third-party YAML.

That is why this is enhancement rather than bug: the alert is real and worth closing, the runtime exposure today is not.

The Fix

Two changes, because the transitive pin alone leaves Dependabot correctly unsatisfied.

1. Raise the direct declared range's floor. js-yaml: ^5.2.0 resolved to a safe 5.2.3 — but Dependabot reads the declared range, not the resolved version, and ^5.2.0 still admits 5.2.0, which is the quadratic build. A fresh install against different registry state could land there. The floor moves to the first fixed release:

"js-yaml": "^5.2.3"

2. Pin the nested copy with an npm overrides entry forcing it to the patched 3.x line, keeping the API gray-matter requires:

"overrides": {
  "gray-matter": {
    "js-yaml": "^3.15.1"
  }
}

Scoped to gray-matter rather than global, so our own direct js-yaml is untouched and a future direct consumer cannot be silently dragged onto 3.x.

Why both. Change 2 removes the vulnerable installed copy; change 1 removes the vulnerable declarable one. Either alone leaves a true statement the alert is still entitled to make.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
package.json overrides npm overrides pins only gray-matter's nested js-yaml to ^3.15.1 none needed; npm fails loudly on an unsatisfiable override inline comment in the ticket, not the manifest npm ls js-yaml shows no 3.15.0
gray-matter front-matter API lib/engines.js:16-17 UNCHANGEDsafeLoad/safeDump still present in 3.15.1 functional parse against 3.15.1
direct js-yaml declared range package.json dependencies ^5.2.0^5.2.3 — the floor moves off the quadratic build; the resolved version stays 5.2.3, so nothing reinstalls none needed; 5.2.3 already satisfies both ranges npm ls js-yaml root entry unchanged at 5.2.3 across the edit
root js-yaml resolution the lockfile UNCHANGED at 5.2.3 the scoped override cannot reach it, and the floor raise does not move it

Decision Record impact

none. A transitive pin, no architectural surface.

Acceptance Criteria

  • npm ls js-yaml --all reports no js-yaml@3.15.0, and gray-matter's nested copy is >= 3.15.1.
  • The root js-yaml resolution remains 5.2.3 — the override must not widen. Control: asserting only that 3.15.0 is gone would also pass if the override had dragged everything onto 3.x.
  • The direct declared range is ^5.2.3, not ^5.2.0. The resolved version is identical either way, so this is the arm that would silently pass on the old range — Dependabot reads the range, and ^5.2.0 still admits the quadratic 5.2.0.
  • gray-matter still parses front matter: the four *Syncer.spec.mjs / verifyFrontmatterIntegrity.spec.mjs arms that consume it stay green, proving safeLoad survived the pin.

Non-gating post-merge observation — deliberately not an acceptance criterion. Dependabot alert 221 should close on GitHub's next scan of dev. It is excluded from the AC set because the alert is its own watcher: it stays open until the dependency graph shows no vulnerable version and re-opens if one returns, so a human residual owner would add a second, worse observer of a fact the platform already tracks. If it does not close, that is a fresh finding about the fix rather than an unticked box on this ticket.

Out of Scope

  • Replacing gray-matter. It is unmaintained and pinned to a dead major, which is a real tech-debt lane — but a dependency swap is not a security patch, and bundling them would make this un-reviewable. Worth its own ticket if the next advisory lands on the same package.
  • The root js-yaml version. Already safe.
  • js-yaml@4.x. Still affected per the advisory, and irrelevant to us — nothing in the tree uses it.

Avoided Traps

  • "Upgrade to latest." The direct dep already is latest-major; the vulnerable copy is transitive, and the latest gray-matter still pins the dead line.
  • Global overrides. Would force our own 5.2.3 down to 3.x, silently reverting a safe dependency to a vulnerable major to fix a vulnerability.
  • Trusting the advisory's "not backported" line. It is wrong for 3.x; the published 3.15.1 carries the fix. Read the code, not the metadata.

Related

Dependabot alert 221 · GHSA-5p4m-2wfm-xmqj.

Retrieval Hint: query_raw_memories("js-yaml omap quadratic gray-matter transitive override 3.15.1")

Live latest-open sweep: checked the latest 20 open issues at 2026-08-21T12:34:18Z plus a keyword search for js-yaml / dependabot / gray-matter; no equivalent ticket and no in-flight claim.

Origin Session ID: ab15d2b8-eb14-4237-ad18-ce48584b2d07

tobiu referenced in commit 31607e5 - "fix(build): pin gray-matter onto the patched js-yaml 3.x line (#17461) (#17462) on Aug 21, 2026, 6:07 PM
tobiu closed this issue on Aug 21, 2026, 6:07 PM