Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

markdown-xss.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

markdown-xss.test.tsBlame105 lines · 1 contributor
5bb52faccanty labs1/**
2 * Security regression — stored XSS in markdown rendering.
3 *
4 * `renderMarkdown()` feeds untrusted markdown (READMEs, issues, PR comments,
5 * wikis, discussions, releases) into ~30 `dangerouslySetInnerHTML` sinks.
6 * These tests lock in that the final `sanitize-html` stage strips executable
7 * payloads while preserving normal markdown + our syntax-highlighted code
8 * blocks. Do not weaken these assertions without a security review.
9 */
10
11import { describe, it, expect } from "bun:test";
12import { renderMarkdown } from "../lib/markdown";
13
14describe("renderMarkdown — XSS sanitization", () => {
15 it("strips raw <script> tags and their payload", () => {
16 const out = renderMarkdown("<script>alert(1)</script>");
17 expect(out).not.toContain("<script");
18 expect(out).not.toContain("alert(1)");
19 });
20
21 it("strips img onerror handlers", () => {
22 const out = renderMarkdown("<img src=x onerror=alert(1)>");
23 expect(out).not.toContain("onerror");
24 expect(out.toLowerCase()).not.toContain("onerror=");
25 });
26
27 it("strips svg onload handlers", () => {
28 const out = renderMarkdown("<svg onload=alert(1)></svg>");
29 expect(out.toLowerCase()).not.toContain("onload");
30 expect(out).not.toContain("<svg");
31 });
32
33 it("strips iframes with javascript: src", () => {
34 const out = renderMarkdown('<iframe src="javascript:alert(1)"></iframe>');
35 expect(out).not.toContain("<iframe");
36 expect(out.toLowerCase()).not.toContain("javascript:");
37 });
38
39 it("neutralizes javascript: links in markdown link syntax", () => {
40 const out = renderMarkdown("[x](javascript:alert(1))");
41 // No executable javascript: URL should survive in an href.
42 expect(/href=["']?\s*javascript:/i.test(out)).toBe(false);
43 expect(out.toLowerCase()).not.toContain("javascript:alert");
44 });
45
46 it("strips inline event handlers on otherwise-allowed tags", () => {
47 const out = renderMarkdown('<a href="https://example.com" onclick="alert(1)">x</a>');
48 expect(out.toLowerCase()).not.toContain("onclick");
49 });
50
51 it("drops <style>, <object>, <embed>, <form> tags", () => {
52 const out = renderMarkdown(
53 "<style>body{display:none}</style><object data=x></object><embed src=x><form></form>",
54 );
55 expect(out).not.toContain("<style");
56 expect(out).not.toContain("<object");
57 expect(out).not.toContain("<embed");
58 expect(out).not.toContain("<form");
59 });
60
61 it("disallows data: URIs on images", () => {
62 const out = renderMarkdown("![x](data:text/html,<script>alert(1)</script>)");
63 expect(out.toLowerCase()).not.toContain("data:text/html");
64 });
65});
66
67describe("renderMarkdown — normal rendering preserved", () => {
68 it("renders headings", () => {
69 const out = renderMarkdown("# Hello");
70 expect(out).toContain("<h1");
71 expect(out).toContain("Hello");
72 });
73
74 it("renders bold text", () => {
75 const out = renderMarkdown("**bold**");
76 expect(out).toContain("<strong>bold</strong>");
77 });
78
79 it("renders safe http links", () => {
80 const out = renderMarkdown("[site](https://example.com)");
81 expect(out).toContain('href="https://example.com"');
82 });
83
84 it("renders images from http(s) sources", () => {
85 const out = renderMarkdown("![alt](https://example.com/a.png)");
86 expect(out).toContain('src="https://example.com/a.png"');
87 expect(out).toContain('alt="alt"');
88 });
89
90 it("preserves syntax-highlighted fenced code blocks", () => {
91 const out = renderMarkdown("```js\nconst x = 1;\n```");
92 expect(out).toContain("<pre>");
93 expect(out).toContain("<code");
94 // language class must survive so highlight.js CSS applies.
95 expect(out).toContain('class="language-js"');
96 // highlight.js emits hljs-* spans; the class attribute must survive.
97 expect(out).toContain("hljs-");
98 expect(out).toContain('class="hljs-');
99 });
100
101 it("renders inline code", () => {
102 const out = renderMarkdown("use `bun test` here");
103 expect(out).toContain("<code>bun test</code>");
104 });
105});