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| Policy | Raw 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,textareaare 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 (hrefona,src/altonimg,colspanon cells…). Custom elements keep all their attributes, since those are their inputs. - Always removed: event handlers (
on*),srcdoc,srcset, andstyleunlessallowStyleis 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 javascript: 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
MarkdownServiceis bound withoutDomSanitizer, which would strip custom elements and Shiki's styles. The policy above is what protects it.