shikidown
Guides

Content security

How shikidown filters raw HTML and URLs, and how to relax the policy for trusted content.

Markdown produced by an LLM — or typed by a user — is untrusted input. A model can be led to reproduce <img src=x onerror=…> or a javascript: link, and anything that reaches the DOM runs in your page. Since 3.0, shikidown filters raw HTML by default.

The HTML policy

provideMarkdown({ htmlPolicy: 'sanitize' }); // default
PolicyRaw HTML in the Markdown
'sanitize' (default)Filtered against an allow-list.
'escape'Displayed as text. Embedded components are disabled.
'trusted'Inserted untouched — the 2.x behaviour. Only for content your application fully controls.

The filter works on the HTML fragments markdown-it isolates, without a DOM, so it runs during server-side rendering too. It does not touch HTML generated by markdown-it itself or by your plugins — those are chosen by your application.

What 'sanitize' keeps

  • Formatting and structure tags: p, div, span, headings, lists, tables, details/summary, code, pre, kbd, img, a, and similar. script, style, iframe, object, embed, template, textarea are removed with their content; other unknown tags (form, svg, input…) are removed and their text kept.
  • Custom elements — any valid name by default. Only the ones your application registered are instantiated; the others stay inert. Restrict them with sanitize.customElements.
  • Attributes: id, class, title, lang, dir, role, aria-*, data-*, plus each tag's own (href on a, src/alt on img, colspan on cells…). Custom elements keep all their attributes, since those are their inputs.
  • Always removed: event handlers (on*), srcdoc, srcset, and style unless allowStyle is set.
  • rel="noopener noreferrer" is forced on links that open a new tab.

URLs

href, src and other URL attributes — on standard and custom elements alike — and Markdown links and images must be relative, an anchor, or use an allowed protocol (http, https, mailto, tel by default). Images may also be data:image/(gif|png|jpeg|webp). Entities and control characters are decoded before the check, so jav&#x61;script: is caught.

Options

provideMarkdown({
  sanitize: {
    allowedProtocols: ['https', 'mailto'],
    allowedTags: ['center'],
    allowStyle: false,
    customElements: ['demo-counter', 'demo-alert'],
  },
});

sanitizeHtml() and isSafeUrl() are exported if you need the same rules elsewhere.

Mermaid

MermaidDirective uses securityLevel: 'strict' by default: HTML in labels is encoded and click handlers are disabled. Opt into 'loose' only for diagrams you trust:

<shikidown mermaid mermaidSecurityLevel="loose" [content]="trustedDoc" />

What this does not cover

  • Plugins render what they render. A plugin that copies user text into attributes is your responsibility.
  • A registered custom element receives the attributes the Markdown gives it — as strings. Treat them as untrusted inputs in the component.
  • The HTML returned by MarkdownService is bound without DomSanitizer, which would strip custom elements and Shiki's styles. The policy above is what protects it.

On this page