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,