import { Marked } from "marked"; const removedElementNames = ["script", "style", "iframe", "object", "embed", "link", "meta", "base", "svg"]; const removedElementsSelector = removedElementNames.join(","); const urlAttributeNames = new Set(["href", "src", "xlink:href", "action", "formaction", "poster", "background"]); const blockedUrlProtocols = ["javascript:", "vbscript:", "data:"]; const anchorRelTokens = ["noopener", "noreferrer"]; // Render markdown to HTML with raw embedded HTML ESCAPED rather than passed // through. marked is not a security sanitizer: by default it emits author-typed // block/inline HTML verbatim, which is the actual stored-XSS vector (e.g. // ``). Escaping raw HTML at the marked layer neutralizes // tag injection regardless of which downstream sanitizer path runs (the server // SSR string fallback below does not use a real HTML parser). sanitizeMarkedHtml // is still chained afterwards to strip unsafe URL protocols (e.g. // `[x](javascript:...)`) from marked-generated links and to add rel hardening. function escapeRawHtml(value: string) { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } const markdownRenderer = new Marked({ renderer: { html: (token) => escapeRawHtml(typeof token === "string" ? token : token?.text ?? token?.raw ?? ""), }, }); export function renderSanitizedMarkdown(markdown: string | null | undefined) { return sanitizeMarkedHtml(markdownRenderer.parse(markdown ?? "", { async: false }) as string); } export function sanitizeMarkedHtml(html: string | null | undefined) { const unsafeHtml = html ?? ""; if (typeof DOMParser !== "undefined") { return sanitizeWithDomParser(unsafeHtml); } return sanitizeWithStringFallback(unsafeHtml); } // Inline SVG icons are stored as raw markup and injected with // dangerouslySetInnerHTML, so they must be sanitized before render. Scriptable // SVG (event handlers,