@xexr/blog-platform 1.0.16 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/blog/[slug]/page.mdx-security.test.ts +135 -0
- package/dist/index.js +11 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -11
- package/dist/index.mjs.map +1 -1
- package/dist/lib/mdx.ts +46 -2
- package/package.json +3 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderToPipeableStream } from "react-dom/server";
|
|
3
|
+
import { PassThrough } from "stream";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import ArticlePage from "./page";
|
|
7
|
+
import type { PublishedArticle } from "../../../types/contracts";
|
|
8
|
+
|
|
9
|
+
const article: PublishedArticle = {
|
|
10
|
+
id: "article-security",
|
|
11
|
+
slug: "mdx-security",
|
|
12
|
+
title: "MDX security",
|
|
13
|
+
excerpt: "Renderer hardening fixtures.",
|
|
14
|
+
content: "Safe content.",
|
|
15
|
+
readingSeconds: 60,
|
|
16
|
+
wordCount: 120,
|
|
17
|
+
publishedAt: "2026-05-13T09:00:00.000Z",
|
|
18
|
+
updatedAt: "2026-05-13T09:00:00.000Z",
|
|
19
|
+
author: null,
|
|
20
|
+
site: {
|
|
21
|
+
id: "site-1",
|
|
22
|
+
name: "Example Blog",
|
|
23
|
+
tagline: "Useful writing",
|
|
24
|
+
description: "Articles about useful systems.",
|
|
25
|
+
domain: "example.com",
|
|
26
|
+
toneAdjectives: [],
|
|
27
|
+
locale: "en",
|
|
28
|
+
},
|
|
29
|
+
topics: [],
|
|
30
|
+
meta: {
|
|
31
|
+
canonicalUrl: "https://example.com/blog/mdx-security",
|
|
32
|
+
keywords: [],
|
|
33
|
+
},
|
|
34
|
+
version: 1,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("ArticlePage MDX security", () => {
|
|
38
|
+
it("renders expected markdown and figure HTML through the real article renderer", async () => {
|
|
39
|
+
const html = await renderToHtml(
|
|
40
|
+
React.createElement(ArticlePage, {
|
|
41
|
+
article: {
|
|
42
|
+
...article,
|
|
43
|
+
content: [
|
|
44
|
+
"## Safe section",
|
|
45
|
+
"",
|
|
46
|
+
"<figure>",
|
|
47
|
+
"",
|
|
48
|
+
"",
|
|
49
|
+
"",
|
|
50
|
+
'<figcaption>Dashboard caption <span class="blog-image-credit">Credit: Example</span></figcaption>',
|
|
51
|
+
"",
|
|
52
|
+
"</figure>",
|
|
53
|
+
"",
|
|
54
|
+
"[Read more](https://example.com/read-more)",
|
|
55
|
+
].join("\n"),
|
|
56
|
+
},
|
|
57
|
+
showShareLinks: false,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(html).toContain("Safe section");
|
|
62
|
+
expect(html).toContain("<figure>");
|
|
63
|
+
expect(html).toContain("<figcaption>");
|
|
64
|
+
expect(html).toContain("Dashboard caption");
|
|
65
|
+
expect(html).toContain('src="https://example.com/dashboard.png"');
|
|
66
|
+
expect(html).toContain('href="https://example.com/read-more"');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("removes executable raw HTML and unsafe URL protocols from rendered articles", async () => {
|
|
70
|
+
const html = await renderToHtml(
|
|
71
|
+
React.createElement(ArticlePage, {
|
|
72
|
+
article: {
|
|
73
|
+
...article,
|
|
74
|
+
content: [
|
|
75
|
+
"Safe paragraph.",
|
|
76
|
+
"",
|
|
77
|
+
"<script>alert('script')</script>",
|
|
78
|
+
'<img src="https://example.com/safe.png" onerror="alert(1)" alt="event handler" />',
|
|
79
|
+
'<a href="javascript:alert(2)">unsafe link</a>',
|
|
80
|
+
'<img src="data:text/html,<script>alert(3)</script>" alt="data url" />',
|
|
81
|
+
'<iframe src="https://example.com/embed"></iframe>',
|
|
82
|
+
'<object data="https://example.com/payload"></object>',
|
|
83
|
+
'<embed src="https://example.com/payload" />',
|
|
84
|
+
].join("\n"),
|
|
85
|
+
},
|
|
86
|
+
showShareLinks: false,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const articleBodyHtml = extractArticleBodyHtml(html);
|
|
91
|
+
|
|
92
|
+
expect(articleBodyHtml).toContain("Safe paragraph.");
|
|
93
|
+
expect(articleBodyHtml).toContain("unsafe link");
|
|
94
|
+
expect(articleBodyHtml).not.toContain("<script");
|
|
95
|
+
expect(articleBodyHtml).not.toContain("onerror");
|
|
96
|
+
expect(articleBodyHtml).not.toContain("javascript:");
|
|
97
|
+
expect(articleBodyHtml).not.toContain("data:text/html");
|
|
98
|
+
expect(articleBodyHtml).not.toContain("<iframe");
|
|
99
|
+
expect(articleBodyHtml).not.toContain("<object");
|
|
100
|
+
expect(articleBodyHtml).not.toContain("<embed");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function extractArticleBodyHtml(html: string): string {
|
|
105
|
+
const start = html.indexOf('id="article-body"');
|
|
106
|
+
const end = html.indexOf('<script type="application/ld+json"', start);
|
|
107
|
+
|
|
108
|
+
if (start === -1) {
|
|
109
|
+
throw new Error("Article body not found in rendered HTML");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return end === -1 ? html.slice(start) : html.slice(start, end);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderToHtml(element: React.ReactElement): Promise<string> {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
let html = "";
|
|
118
|
+
const stream = new PassThrough();
|
|
119
|
+
stream.setEncoding("utf8");
|
|
120
|
+
stream.on("data", (chunk: string) => {
|
|
121
|
+
html += chunk;
|
|
122
|
+
});
|
|
123
|
+
stream.on("end", () => resolve(html));
|
|
124
|
+
stream.on("error", reject);
|
|
125
|
+
|
|
126
|
+
const { pipe } = renderToPipeableStream(element, {
|
|
127
|
+
onAllReady() {
|
|
128
|
+
pipe(stream);
|
|
129
|
+
},
|
|
130
|
+
onError(error) {
|
|
131
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|