fix(deps): drop gray-matter, parse frontmatter with js-yaml 4 - #24
Merged
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
npm audit --audit-level=highstarted failing on CVE-2026-59870 / GHSA-5p4m-2wfm-xmqj — quadratic CPU consumption in js-yaml's!!omapresolution, affecting3.0.0–3.15.0and4.0.0–4.3.0. This blocks every pull request, not just one.js-yaml reached the project three ways:
@keystatic/core4.3.1is outside the rangeeslint→@eslint/eslintrcgray-matterThe fix was never backported to 3.x,
gray-matter@4.0.3is already the newest published release and has not moved in years, and itslib/engines.jsbindsyaml.safeLoad/yaml.safeDumpat 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.tsparses the---block with js-yaml 4, andsrc/lib/mdx.tscalls it instead ofmatter(). Anoverridesentry pins js-yaml to^4.3.1so the keystatic and eslint paths resolve to the patched build.gray-matterand 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:
gray-matter@4.0.3was installed side by side and both parsers run over all 18 checked-in posts, dumpingdataandcontentwithDatevalues marked distinguishably (Date.prototype.toJSONwould otherwise have hidden exactly the date-vs-string difference that mattered). Result: 0 diffs, identical SHA-256.Dateand quoted ones stay strings — 7 and 11 of the 18 respectively, unchanged. Downstream code branches oninstanceof Date, so this is load-bearing.core.autocrlf=true, no.gitattributes). A parser matching only\nwould have failed on all of them; both fences use\r?\n, confirmed against the real files.---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=high→ 0 vulnerabilities ·npm ls js-yaml→ one deduped4.3.1, no 3.x anywhere ·vitest149/149 ·tsc --noEmitclean ·eslintclean ·next buildsucceeds.🤖 Generated with Claude Code
https://claude.ai/code/session_01LaydoxAZ4eaqqPNKrmWLbt