/** * Isomorphic HTML sanitizer for rendered markdown. * * `marked` allows raw HTML to pass through untouched, and the resulting string * is injected via `set:html` / `innerHTML` on the resource detail pages and in * the client-side file browser. Even though the markdown we render originates * from this repository, a compromised or malicious resource file could * otherwise introduce persistent XSS. Sanitizing the generated HTML gives us * defense-in-depth on both the server (build time) and the client. * * `isomorphic-dompurify` resolves to a jsdom-backed DOMPurify in Node (so it * works in Astro frontmatter during `astro build`) and to the native * browser DOMPurify when bundled for the client. */ import DOMPurify from "isomorphic-dompurify"; let noopenerHookInstalled = false; function ensureNoopenerHook(): void { if (noopenerHookInstalled) return; noopenerHookInstalled = true; DOMPurify.addHook("afterSanitizeAttributes", (node) => { const el = node as unknown as { tagName?: string; getAttribute?: (name: string) => string | null; setAttribute?: (name: string, value: string) => void; }; if (el?.tagName !== "A") return; if (el.getAttribute?.("target") !== "_blank") return; const rel = el.getAttribute?.("rel") ?? ""; const tokens = new Set(rel.split(/\s+/).filter(Boolean)); tokens.add("noopener"); tokens.add("noreferrer"); el.setAttribute?.("rel", Array.from(tokens).join(" ")); }); } /** * Sanitize a fragment of HTML produced from trusted-but-untrusted markdown, * stripping scripts, event handlers, and dangerous URL schemes while keeping * the formatting tags GitHub-flavored markdown commonly emits. */ export function sanitizeHtml(html: string): string { if (!html) return html; ensureNoopenerHook(); return DOMPurify.sanitize(html, { // Keep links that open in a new tab (target/rel) which some resource docs // author directly as raw HTML. ADD_ATTR: ["target", "rel"], }); }