/** * SECURITY: Strip script-bearing constructs from an SVG string before it is * passed to `dangerouslySetInnerHTML` (e.g. via ``). * * This is an allowlist-leaning scrubber that closes the documented XSS * vectors. It assumes the caller produced the SVG from a Mermaid render (or * equivalent server-side renderer) and provides defense-in-depth against: * * - `c` // and other adversarial nesting. let guard = 0 do { prev = next for (const { block, selfClosing } of ELEMENT_REGEXES) { next = next.replace(block, '').replace(selfClosing, '') } guard++ } while (next !== prev && guard < 16) // Final pass: drop any orphaned open/close tags the recursion left behind. for (const { orphanOpen, orphanClose } of ELEMENT_REGEXES) { next = next.replace(orphanOpen, '').replace(orphanClose, '') } return next } function stripDangerousStyles(input: string): string { return input.replace(STYLE_ATTR_RE, (match, rawValue: string) => { // Strip outer quotes for inspection. const unquoted = rawValue.startsWith('"') || rawValue.startsWith("'") ? rawValue.slice(1, -1) : rawValue return CSS_SINK_RE.test(unquoted) ? '' : match }) } export function sanitizeSvg(svg: string): string { if (!svg) return '' let scrubbed = stripForbiddenElements(svg) .replace(EVENT_HANDLER_RE, '') .replace(JAVASCRIPT_URI_QUOTED_RE, '$1$2#$2') .replace(JAVASCRIPT_URI_UNQUOTED_RE, '$1#') scrubbed = stripDangerousStyles(scrubbed) // Defense-in-depth: ensure the root is still an . If the scrub // mangled the structure or the input wasn't an SVG to begin with, drop it. const leading = scrubbed.trimStart() if (!leading.toLowerCase().startsWith('