Skip to content

Migrate scripts/affected.ts from js-yaml to yaml - #2950

Closed
Tommy Nguyen (tido64) with Copilot wants to merge 1 commit into
trunkfrom
copilot/investigate-js-yaml-alternatives
Closed

Tommy Nguyen (tido64) with Copilot wants to merge 1 commit into
trunkfrom
copilot/investigate-js-yaml-alternatives

Conversation

Copilot AI commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces js-yaml with yaml (eemeli/yaml) as the YAML parsing dependency used by scripts/affected.ts to load .github/labeler.yml.

Changes

  • package.json: "js-yaml": "^5.3.0" → "yaml": "^2.9.1" in devDependencies (lockfile updated)
  • scripts/affected.ts: import * as yaml from "js-yaml" → import * as yaml from "yaml"; yaml.load(yml) → yaml.parse(yml) (near drop-in, only read/parse is used — no dumping)

Why

Investigated because new vulnerabilities keep being discovered in js-yaml, generating alert noise for a narrow, low-risk use case (parsing a single static, trusted, repo-owned config file once per CI job — no untrusted input).

Security track record

Package Advisory history
js-yaml ~12 known advisories over its lifetime: RCE via unsafe deserialization (CVE-2013-4660), several quadratic/exponential-complexity DoS bugs (!!omap, merge-key << chains, flow collections — recurring, including in the current 5.x line), and prototype pollution via merge keys (CVE-2025-64718)
yaml 1 known advisory: stack overflow via deeply nested collections (CVE-2026-33532), cleanly patched in 1.10.3/2.8.3

yaml also has zero transitive runtime dependencies (removes the argparse sub-dependency js-yaml carries for its CLI, which we don't use).

Performance caveat

yaml is measurably slower than js-yaml, benchmarked against the actual .github/labeler.yml file on Node v22:

Fresh-process (matches real usage — affected.ts runs once per CI job as a new node process):

Library require() parse Total
js-yaml ~6.6 ms ~5.1 ms ~11.7 ms
yaml ~25.3 ms ~11.5 ms ~36.8 ms

yaml's CJS build is split across ~74 files vs. js-yaml's 2, so require() costs ~4x more; yaml.parse() itself is ~2.3x slower than js-yaml.load() since it builds a full CST before producing the JS value.

Warm/JIT-optimized (20,000 iterations, order-independent):

Library Avg/call Ops/sec
js-yaml load() ~0.10 ms ~9,400–10,400
yaml parse() ~0.90 ms ~1,080–1,110

Practical impact: since affected.ts parses once per fresh process, only the fresh-process numbers apply — an increase of ~25 ms per CI job invocation, negligible next to typical CI job durations. The security/dependency benefits outweigh this cost for our use case.

Validation

  • yarn show-affected still parses .github/labeler.yml correctly and falls back to matching all platforms when base commit isn't resolvable (expected in a shallow clone)
  • oxlint (yarn lint:js) — clean
  • knip — no unused/missing dependency issues
  • No remaining js-yaml references in the repo
  • No secrets introduced (scanned changed files)
  • CodeQL security scan and code review validation passed (change assessed as trivial: equivalent parse semantics on the same trusted, static file)

Co-authored-by: tido64 <4123478+tido64@users.noreply.github.com>
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.

2 participants