fix: replace gray-matter with direct yaml.load for js-yaml 4.x compatibility (#26555)
gray-matter uses yaml.safeLoad() which was removed in js-yaml 4.x, causing 500 errors on app store pages after the js-yaml 4.1.1 update (CWE-1321 fix) - Add parseFrontmatter function using yaml.load with JSON_SCHEMA - Add type guard for safe type narrowing - Add unit tests for frontmatter parsing and security - Remove gray-matter dependency
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseFrontmatter } from "../getStaticProps";
|
||||
|
||||
describe("parseFrontmatter", () => {
|
||||
describe("valid frontmatter parsing", () => {
|
||||
it("parses frontmatter with items array", () => {
|
||||
const source = `---
|
||||
items:
|
||||
- 1.jpg
|
||||
- 2.png
|
||||
---
|
||||
Some content here`;
|
||||
|
||||
const result = parseFrontmatter(source);
|
||||
|
||||
expect(result.data).toEqual({ items: ["1.jpg", "2.png"] });
|
||||
expect(result.content).toBe("Some content here");
|
||||
});
|
||||
|
||||
it("parses frontmatter with description only", () => {
|
||||
const source = `---
|
||||
description: A simple description
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = parseFrontmatter(source);
|
||||
|
||||
expect(result.data).toEqual({ description: "A simple description" });
|
||||
expect(result.content).toBe("Content");
|
||||
});
|
||||
|
||||
it("parses items with iframe objects", () => {
|
||||
const source = `---
|
||||
items:
|
||||
- iframe: { src: https://youtube.com/embed/abc }
|
||||
- 1.jpg
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = parseFrontmatter(source);
|
||||
|
||||
expect(result.data).toEqual({
|
||||
items: [{ iframe: { src: "https://youtube.com/embed/abc" } }, "1.jpg"],
|
||||
});
|
||||
expect(result.content).toBe("Content");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("returns empty data when no frontmatter present", () => {
|
||||
const source = "Just plain text content";
|
||||
|
||||
const result = parseFrontmatter(source);
|
||||
|
||||
expect(result.data).toEqual({});
|
||||
expect(result.content).toBe("Just plain text content");
|
||||
});
|
||||
|
||||
it("parses simple frontmatter correctly", () => {
|
||||
const source = `---
|
||||
items:
|
||||
- test.jpg
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = parseFrontmatter(source);
|
||||
|
||||
expect(result.data).toEqual({ items: ["test.jpg"] });
|
||||
expect(result.content).toBe("Content");
|
||||
});
|
||||
|
||||
it("preserves blank line after frontmatter", () => {
|
||||
const source = `---
|
||||
title: Test
|
||||
---
|
||||
|
||||
Content`;
|
||||
|
||||
const result = parseFrontmatter(source);
|
||||
|
||||
expect(result.data).toEqual({ title: "Test" });
|
||||
expect(result.content).toBe("\nContent");
|
||||
});
|
||||
|
||||
it("returns empty data for non-object frontmatter (array root)", () => {
|
||||
const source = `---
|
||||
- item1
|
||||
- item2
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = parseFrontmatter(source);
|
||||
|
||||
expect(result.data).toEqual({});
|
||||
expect(result.content).toBe("Content");
|
||||
});
|
||||
});
|
||||
|
||||
describe("security (JSON_SCHEMA protection)", () => {
|
||||
it("returns empty data on unsafe YAML types", () => {
|
||||
const source = `---
|
||||
date: !!js/date 2024-01-01
|
||||
---
|
||||
Content`;
|
||||
|
||||
const result = parseFrontmatter(source);
|
||||
|
||||
expect(result.data).toEqual({});
|
||||
expect(result.content).toBe("Content");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,51 @@
|
||||
import fs from "node:fs"
|
||||
import matter from "gray-matter";
|
||||
import path from "node:path"
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["lib", "parseFrontmatter"] });
|
||||
|
||||
const FRONTMATTER_REGEX = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses markdown content with YAML frontmatter
|
||||
* Replaces gray-matter to use js-yaml 4.x directly (yaml.load is safe by default)
|
||||
*/
|
||||
export function parseFrontmatter(source: string): { data: Record<string, unknown>; content: string } {
|
||||
const match = source.match(FRONTMATTER_REGEX);
|
||||
|
||||
if (!match) {
|
||||
return { data: {}, content: source };
|
||||
}
|
||||
|
||||
let data: Record<string, unknown> = {};
|
||||
|
||||
try {
|
||||
const parsed = yaml.load(match[1], { schema: yaml.JSON_SCHEMA });
|
||||
|
||||
if (isRecord(parsed)) {
|
||||
data = parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn("Invalid YAML frontmatter", { error });
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
content: source.slice(match[0].length),
|
||||
};
|
||||
}
|
||||
|
||||
export const sourceSchema = z.object({
|
||||
content: z.string(),
|
||||
@@ -65,7 +104,7 @@ export const getStaticProps = async (slug: string) => {
|
||||
source = appMeta.description;
|
||||
}
|
||||
|
||||
const result = matter(source);
|
||||
const result = parseFrontmatter(source);
|
||||
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||
if (data.items) {
|
||||
data.items = data.items.map((item) => {
|
||||
|
||||
@@ -91,7 +91,6 @@
|
||||
"classnames": "2.3.2",
|
||||
"dompurify": "3.3.1",
|
||||
"entities": "4.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
"handlebars": "4.7.7",
|
||||
"i18next": "23.2.3",
|
||||
"ical.js": "1.5.0",
|
||||
|
||||
Reference in New Issue
Block a user