Skip to content

fix(deps): drop gray-matter, parse frontmatter with js-yaml 4 - #24

Merged
writingdeveloper merged 2 commits into
mainfrom
fix/js-yaml-advisory
Aug 7, 2026
Merged

fix(deps): drop gray-matter, parse frontmatter with js-yaml 4#24
writingdeveloper merged 2 commits into
mainfrom
fix/js-yaml-advisory

Conversation

@writingdeveloper

Copy link
Copy Markdown
Owner

Why

npm audit --audit-level=high started failing on CVE-2026-59870 / GHSA-5p4m-2wfm-xmqj — quadratic CPU consumption in js-yaml's !!omap resolution, affecting 3.0.0–3.15.0 and 4.0.0–4.3.0. This blocks every pull request, not just one.

js-yaml reached the project three ways:

via version fixable?
@keystatic/core 4.3.0 yes — 4.3.1 is outside the range
eslint@eslint/eslintrc 4.3.0 same
gray-matter 3.15.0 no

The fix was never backported to 3.x, gray-matter@4.0.3 is already the newest published release and has not moved in years, and its lib/engines.js binds yaml.safeLoad/yaml.safeDump at module load — functions js-yaml 4 removed. So an override crashes it at import, and there was no upgrade path. The dependency had to go.

It had exactly one call site.

What

src/lib/frontmatter.ts parses the --- block with js-yaml 4, and src/lib/mdx.ts calls it instead of matter(). An overrides entry pins js-yaml to ^4.3.1 so the keystatic and eslint paths resolve to the patched build. gray-matter and its ten transitive packages are gone from the lockfile.

How it was proven safe

Swapping a battle-tested parser for hand-written parsing over every blog post is the kind of change that breaks things silently, so it was verified by differential testing rather than by reading:

  • Real gray-matter@4.0.3 was installed side by side and both parsers run over all 18 checked-in posts, dumping data and content with Date values marked distinguishably (Date.prototype.toJSON would otherwise have hidden exactly the date-vs-string difference that mattered). Result: 0 diffs, identical SHA-256.
  • Unquoted YAML dates still resolve to Date and quoted ones stay strings — 7 and 11 of the 18 respectively, unchanged. Downstream code branches on instanceof Date, so this is load-bearing.
  • Every post in this repo is checked out CRLF (core.autocrlf=true, no .gitattributes). A parser matching only \n would have failed on all of them; both fences use \r?\n, confirmed against the real files.
  • Delimiter edge cases were hand-walked against gray-matter: a --- horizontal rule in the body (2 posts genuinely have one) is preserved, an unterminated block returns the file rather than throwing, and a ---\n-only file no longer gets eaten. Where behaviour differs from gray-matter it is strictly safer, never lossier.

13 regression tests cover frontmatter present, absent, BOM-prefixed, unquoted dates, nested arrays-of-objects, a body ---, bare scalars, and top-level arrays.

Verification

npm audit --audit-level=high0 vulnerabilities · npm ls js-yaml → one deduped 4.3.1, no 3.x anywhere · vitest 149/149 · tsc --noEmit clean · eslint clean · next build succeeds.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LaydoxAZ4eaqqPNKrmWLbt

writingdeveloper and others added 2 commits August 6, 2026 20:38
CVE-2026-59870 / GHSA-5p4m-2wfm-xmqj is quadratic CPU consumption in
js-yaml's !!omap resolution, affecting 3.0.0-3.15.0 and 4.0.0-4.3.0.
js-yaml reached us three ways: @keystatic/core and eslint on 4.3.0, and
gray-matter on 3.15.0.

The first two are an overrides entry away from the patched 4.3.1. The
third had no path out. The fix was never backported to 3.x, gray-matter
4.0.3 is the latest published version and has been idle for years, and
its engines module binds yaml.safeLoad/safeDump at import — functions
js-yaml 4 removed — so forcing the override on it crashes at load.

So gray-matter goes. It had exactly one call site, and the replacement
only needs the two things that call site used.

Preserving its behaviour mattered more than it looks: it strips a BOM
before matching the fence, and unquoted YAML dates arrive as Date
objects. Seven of the eighteen posts leave publishedAt unquoted and
getAllPosts sorts on it, so js-yaml's default schema has to stay as-is
and mdx.ts has to keep handing those values through uncoerced — hence
verbatim() being a cast rather than a String() call.

Verified by parsing all eighteen posts with both parsers and diffing
the dumps, with Date values marked distinguishably from ISO strings.
Byte-identical.

The new parser is deliberately stricter in three unreachable corners:
it ignores language-tagged fences (gray-matter dispatches those to an
eval-backed javascript engine), returns the whole document rather than
eating it when a block is unterminated, and never returns a non-object
as data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaydoxAZ4eaqqPNKrmWLbt
… branches

verbatim() blanket-cast all eight PostMeta string fields; only publishedAt
can hold a non-string (Date, from an unquoted YAML date). Split it so seven
fields type-check via a real typeof guard and only publishedAt keeps the
escape-hatch cast, with a comment on why. Also add regression tests for the
frontmatter.ts non-object guard's two untested branches: a bare-scalar block
and a top-level-array block, both of which must still yield data = {}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LaydoxAZ4eaqqPNKrmWLbt
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
portfolio Ready Ready Preview Aug 7, 2026 3:49am

Request Review

@writingdeveloper
writingdeveloper merged commit e48d365 into main Aug 7, 2026
3 checks passed
@writingdeveloper
writingdeveloper deleted the fix/js-yaml-advisory branch August 7, 2026 06:50
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.

1 participant