Skip to content

Commit 2aed990

Browse files
authored
fix: replace gray-matter with direct yaml.load for js-yaml 4.x compatibility (calcom#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
1 parent f54a3d1 commit 2aed990

4 files changed

Lines changed: 156 additions & 7 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, expect, it } from "vitest";
2+
import { parseFrontmatter } from "../getStaticProps";
3+
4+
describe("parseFrontmatter", () => {
5+
describe("valid frontmatter parsing", () => {
6+
it("parses frontmatter with items array", () => {
7+
const source = `---
8+
items:
9+
- 1.jpg
10+
- 2.png
11+
---
12+
Some content here`;
13+
14+
const result = parseFrontmatter(source);
15+
16+
expect(result.data).toEqual({ items: ["1.jpg", "2.png"] });
17+
expect(result.content).toBe("Some content here");
18+
});
19+
20+
it("parses frontmatter with description only", () => {
21+
const source = `---
22+
description: A simple description
23+
---
24+
Content`;
25+
26+
const result = parseFrontmatter(source);
27+
28+
expect(result.data).toEqual({ description: "A simple description" });
29+
expect(result.content).toBe("Content");
30+
});
31+
32+
it("parses items with iframe objects", () => {
33+
const source = `---
34+
items:
35+
- iframe: { src: https://youtube.com/embed/abc }
36+
- 1.jpg
37+
---
38+
Content`;
39+
40+
const result = parseFrontmatter(source);
41+
42+
expect(result.data).toEqual({
43+
items: [{ iframe: { src: "https://youtube.com/embed/abc" } }, "1.jpg"],
44+
});
45+
expect(result.content).toBe("Content");
46+
});
47+
});
48+
49+
describe("edge cases", () => {
50+
it("returns empty data when no frontmatter present", () => {
51+
const source = "Just plain text content";
52+
53+
const result = parseFrontmatter(source);
54+
55+
expect(result.data).toEqual({});
56+
expect(result.content).toBe("Just plain text content");
57+
});
58+
59+
it("parses simple frontmatter correctly", () => {
60+
const source = `---
61+
items:
62+
- test.jpg
63+
---
64+
Content`;
65+
66+
const result = parseFrontmatter(source);
67+
68+
expect(result.data).toEqual({ items: ["test.jpg"] });
69+
expect(result.content).toBe("Content");
70+
});
71+
72+
it("preserves blank line after frontmatter", () => {
73+
const source = `---
74+
title: Test
75+
---
76+
77+
Content`;
78+
79+
const result = parseFrontmatter(source);
80+
81+
expect(result.data).toEqual({ title: "Test" });
82+
expect(result.content).toBe("\nContent");
83+
});
84+
85+
it("returns empty data for non-object frontmatter (array root)", () => {
86+
const source = `---
87+
- item1
88+
- item2
89+
---
90+
Content`;
91+
92+
const result = parseFrontmatter(source);
93+
94+
expect(result.data).toEqual({});
95+
expect(result.content).toBe("Content");
96+
});
97+
});
98+
99+
describe("security (JSON_SCHEMA protection)", () => {
100+
it("returns empty data on unsafe YAML types", () => {
101+
const source = `---
102+
date: !!js/date 2024-01-01
103+
---
104+
Content`;
105+
106+
const result = parseFrontmatter(source);
107+
108+
expect(result.data).toEqual({});
109+
expect(result.content).toBe("Content");
110+
});
111+
});
112+
});

apps/web/lib/apps/[slug]/getStaticProps.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,51 @@
1-
import fs from "node:fs"
2-
import matter from "gray-matter";
3-
import path from "node:path"
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
import yaml from "js-yaml";
45
import { z } from "zod";
56

67
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
78
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
89
import { IS_PRODUCTION } from "@calcom/lib/constants";
9-
import prisma from "@calcom/prisma";
10+
import { prisma } from "@calcom/prisma";
11+
import logger from "@calcom/lib/logger";
12+
13+
const log = logger.getSubLogger({ prefix: ["lib", "parseFrontmatter"] });
14+
15+
const FRONTMATTER_REGEX = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
16+
17+
function isRecord(value: unknown): value is Record<string, unknown> {
18+
return value !== null && typeof value === "object" && !Array.isArray(value);
19+
}
20+
21+
/**
22+
* Parses markdown content with YAML frontmatter
23+
* Replaces gray-matter to use js-yaml 4.x directly (yaml.load is safe by default)
24+
*/
25+
export function parseFrontmatter(source: string): { data: Record<string, unknown>; content: string } {
26+
const match = source.match(FRONTMATTER_REGEX);
27+
28+
if (!match) {
29+
return { data: {}, content: source };
30+
}
31+
32+
let data: Record<string, unknown> = {};
33+
34+
try {
35+
const parsed = yaml.load(match[1], { schema: yaml.JSON_SCHEMA });
36+
37+
if (isRecord(parsed)) {
38+
data = parsed;
39+
}
40+
} catch (error) {
41+
log.warn("Invalid YAML frontmatter", { error });
42+
}
43+
44+
return {
45+
data,
46+
content: source.slice(match[0].length),
47+
};
48+
}
1049

1150
export const sourceSchema = z.object({
1251
content: z.string(),
@@ -65,7 +104,7 @@ export const getStaticProps = async (slug: string) => {
65104
source = appMeta.description;
66105
}
67106

68-
const result = matter(source);
107+
const result = parseFrontmatter(source);
69108
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
70109
if (data.items) {
71110
data.items = data.items.map((item) => {

apps/web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@
9191
"classnames": "2.3.2",
9292
"dompurify": "3.3.1",
9393
"entities": "4.5.0",
94-
"gray-matter": "4.0.3",
9594
"handlebars": "4.7.7",
9695
"i18next": "23.2.3",
9796
"ical.js": "1.5.0",

yarn.lock

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3546,7 +3546,6 @@ __metadata:
35463546
env-cmd: "npm:10.1.0"
35473547
glob: "npm:10.4.5"
35483548
google-auth-library: "npm:9.15.0"
3549-
gray-matter: "npm:4.0.3"
35503549
handlebars: "npm:4.7.7"
35513550
i18next: "npm:23.2.3"
35523551
ical.js: "npm:1.5.0"

0 commit comments

Comments
 (0)