Skip to content

Commit c5fa77b

Browse files
ggreifclaudeKamirus
authored
fix(frontend): render raw HTML in package READMEs (sanitized) (#602)
## Problem Package README pages show raw HTML tags as **literal visible text**. For example, [mops.one/googlemail-client@0.1.3](https://mops.one/googlemail-client@0.1.3) renders a `<details>`/`<summary>` collapsible block as the plain strings `<details>` and `<summary>` rather than a working disclosure widget. GitHub renders the same README correctly. ## Root cause `frontend/logic/markdown-to-html.ts` configures markdown-it with `html: false`, which **escapes all raw HTML** in README source. The inline comment even read `// Enable HTML tags in source` — the opposite of what `false` does. GitHub, by contrast, permits a *sanitized allowlist* of tags (including `<details>`/`<summary>`), which is why authors write them. ## Fix Enable `html: true` and sanitize the rendered output with **DOMPurify** — the standard, GitHub-equivalent approach: - Allows a safe tag allowlist (incl. `<details>`/`<summary>`, and keeps `class` so starry-night syntax highlighting is preserved). - Strips `<script>`, inline event handlers, and `javascript:` URLs — so enabling raw HTML does **not** open an XSS hole, which matters since READMEs are arbitrary third-party author content. Two incidental cleanups in the same file: - Corrected the misleading comment to match behavior. - Removed a dead duplicate `mdit.render(markdown)` call whose result was discarded (the source was parsed twice per render). ## Impact / retroactivity README → HTML conversion happens **client-side at view time** (`PackageReadme.svelte` calls `markdownToHtml(readme, …)` on each render), from the stored Markdown source. So this fix repairs **every already-published package page** on the next load — no republish, version bump, or re-upload required. ## Validation - `svelte-check --threshold error` → **0 errors** (pre-existing warnings unchanged). - DOMPurify v3 ships its own types; pinned exact (`3.4.11`) to match the repo's dependency convention. Lockfile gains only the single dependency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Kamil Listopad <listopadkamil@gmail.com>
1 parent 0af0025 commit c5fa77b

4 files changed

Lines changed: 76 additions & 6 deletions

File tree

frontend/components/package/PackageDocs.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import markdownIt from "markdown-it";
99
import "@wooorm/starry-night/style/light";
1010
import {getStarryNight} from "/logic/get-starry-night";
11+
import DOMPurify from "dompurify";
1112
1213
type DefinitionKind = "module" | "class" | "type" | "func" | "value" | "type-actor";
1314
type Definition = {
@@ -136,7 +137,7 @@
136137
};
137138
});
138139
139-
div.innerHTML = doc.convert();
140+
div.innerHTML = DOMPurify.sanitize(doc.convert());
140141
141142
// nested fields
142143
div.querySelectorAll("h3").forEach((el) => {

frontend/logic/markdown-to-html.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import markdownIt from "markdown-it";
2+
import DOMPurify from "dompurify";
23
import { toHtml } from "hast-util-to-html";
34

45
import { getStarryNight } from "./get-starry-night";
@@ -19,7 +20,7 @@ export let markdownToHtml = async (
1920

2021
let div = document.createElement("div");
2122
let mdit = markdownIt({
22-
html: false, // Enable HTML tags in source
23+
html: true, // Allow raw HTML tags in source (e.g. <details>); output is sanitized with DOMPurify below
2324
breaks: false, // Convert '\n' in paragraphs into <br>
2425
linkify: true, // Autoconvert URL-like text to links
2526

@@ -45,9 +46,54 @@ export let markdownToHtml = async (
4546
},
4647
});
4748

48-
mdit.render(markdown);
49-
50-
div.innerHTML = mdit.render(markdown);
49+
div.innerHTML = DOMPurify.sanitize(mdit.render(markdown), {
50+
ALLOWED_TAGS: [
51+
"details",
52+
"summary",
53+
"p",
54+
"br",
55+
"strong",
56+
"b",
57+
"em",
58+
"i",
59+
"s",
60+
"del",
61+
"code",
62+
"pre",
63+
"h1",
64+
"h2",
65+
"h3",
66+
"h4",
67+
"h5",
68+
"h6",
69+
"ul",
70+
"ol",
71+
"li",
72+
"blockquote",
73+
"table",
74+
"thead",
75+
"tbody",
76+
"tr",
77+
"th",
78+
"td",
79+
"img",
80+
"a",
81+
"hr",
82+
"div",
83+
"span",
84+
],
85+
ALLOWED_ATTR: [
86+
"href",
87+
"src",
88+
"alt",
89+
"class",
90+
"id",
91+
"name",
92+
"target",
93+
"rel",
94+
"title",
95+
],
96+
});
5197

5298
// replace relative url to github absolute url
5399
if (repositoryUrl) {

frontend/package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"chart.js": "4.4.2",
2121
"chartjs-adapter-date-fns": "^3.0.0",
2222
"date-fns": "3.6.0",
23+
"dompurify": "3.4.11",
2324
"filesize": "10.1.1",
2425
"hast-util-to-html": "9.0.5",
2526
"ic-mops": "2.14.0",
@@ -32,10 +33,11 @@
3233
},
3334
"devDependencies": {
3435
"@sveltejs/vite-plugin-svelte": "7.0.0",
35-
"@types/throttle-debounce": "5.0.2",
36+
"@types/dompurify": "3.0.5",
3637
"@types/markdown-it": "13.0.7",
3738
"@types/node": "22.15.3",
3839
"@types/pako": "2.0.4",
40+
"@types/throttle-debounce": "5.0.2",
3941
"eslint-plugin-svelte": "2.46.1",
4042
"svelte": "5.55.7",
4143
"svelte-check": "4.4.6",

0 commit comments

Comments
 (0)