Sanitization
Sanitization helps prevent HTML injection attacks (XSS) by filtering dangerous HTML from story args and slots before they’re rendered in Astro components.
Why sanitization matters
Section titled “Why sanitization matters”When story args contain HTML strings—especially from user input or dynamic sources—malicious scripts can be injected:
// ❌ Unsafe: script tags in argsexport const Vulnerable = { args: { content: '<img src=x onerror="alert(\'XSS\')">', },};Sanitization strips dangerous elements while preserving safe HTML:
// ✅ Safe: malicious code removed by sanitizationexport const Safe = { args: { // Script is removed, image tag is preserved (without onerror) content: '<img src=x />', },};Default behavior
Section titled “Default behavior”Sanitization is enabled by default with conservative settings:
- What’s sanitized: All slot strings by default
- What’s preserved: A safe allowlist of HTML tags and attributes
- Dangerous content removed: Scripts, event handlers, dangerous protocols
The default allowlist includes common formatting tags:
- Text formatting:
<b>,<strong>,<em>,<i>,<u>,<s> - Structure:
<p>,<div>,<span>,<br>,<hr> - Lists:
<ul>,<ol>,<li> - Tables:
<table>,<thead>,<tbody>,<tfoot>,<tr>,<th>,<td> - Media:
<img>(src, alt, title, width, height) - Links:
<a>(href, target, rel) - Code:
<code>,<pre>,<kbd> - And more…
Configuration
Section titled “Configuration”Configure sanitization in .storybook/main.js:
export default { framework: { name: '@storybook-astro/framework', options: { sanitization: { enabled: true, args: [], // Don't sanitize any args by default slots: ['**'], // Sanitize all slot strings }, }, },};enabled
Section titled “enabled”Enable or disable sanitization entirely:
sanitization: { enabled: false, // YOLO mode—no sanitization}Use with caution: Only disable if all content is trusted.
Specify which args to sanitize using dot-notation patterns:
sanitization: { args: [ 'content', // Sanitize Astro.props.content 'description', // Sanitize Astro.props.description ],}Useful when story args contain HTML that should be filtered.
Specify which slots to sanitize:
sanitization: { slots: [ 'default', // Sanitize slot.default 'footer', // Sanitize slot.footer ],}Default is ['**'] (all slots).
Wildcard patterns
Section titled “Wildcard patterns”Use wildcards to target nested values:
sanitization: { args: [ 'items.*.html', // Sanitize html in each item 'meta.**.content', // Sanitize content at any depth ],}*matches one segment:items.*.name**matches any depth:data.**.html
Custom sanitize-html options
Section titled “Custom sanitize-html options”Fine-tune the allowlist by passing sanitize-html options:
sanitization: { sanitizeHtml: { allowedTags: ['p', 'strong', 'em', 'a', 'ul', 'li'], allowedAttributes: { a: ['href', 'target'], }, allowedSchemes: ['https', 'mailto'], },}This creates a minimal allowlist (only paragraphs, emphasis, links, and lists).
Merging with defaults
Section titled “Merging with defaults”Custom options are merged with defaults, not replacing them. This means:
sanitizeHtml: { allowedTags: ['custom-tag'], // Added to defaults}Results in both default tags AND custom-tag being allowed.
Common customizations
Section titled “Common customizations”Allow classes and data attributes:
sanitizeHtml: { allowedAttributes: { '*': ['class', 'data-*'], a: ['href', 'target'], },}Allow inline styles:
sanitizeHtml: { allowedStyles: { '*': { color: [/^#(?:[0-9a-fA-F]{3}){1,2}$/], // Hex colors 'font-size': [/^\d+px$/], // Pixel sizes }, },}See the full sanitize-html documentation for all available options: https://www.npmjs.com/package/sanitize-html
Disabling sanitization
Section titled “Disabling sanitization”Explicitly disable for trusted content:
sanitization: { enabled: false,}⚠️ Only disable if all story content comes from trusted sources (not user input or dynamic APIs).
Security best practices
Section titled “Security best practices”- Keep sanitization enabled by default
- Be specific with patterns — don’t over-sanitize
- Whitelist, don’t blacklist — only allow known-safe tags
- Test sanitization — verify dangerous content is removed
- Document exceptions — note when/why sanitization is disabled
- Audit story content — review what HTML is being passed as args
Example: Rich text content
Section titled “Example: Rich text content”A story that accepts rich text HTML:
interface Props { html: string; // User-provided HTML}
const { html } = Astro.props;---
<div class="rich-text" set:html={html} />Configuration:
export default { framework: { name: '@storybook-astro/framework', options: { sanitization: { args: ['html'], // Sanitize the html arg sanitizeHtml: { allowedTags: ['p', 'strong', 'em', 'a', 'ul', 'li', 'blockquote'], allowedAttributes: { a: ['href', 'target', 'rel'], }, }, }, }, },};Story:
import RichText from './RichText.astro';
export default { title: 'Components/RichText', component: RichText,};
export const Safe = { args: { html: '<p>This is <strong>bold</strong> and <em>italic</em> text.</p>', },};
export const WithDangerousCode = { args: { html: '<p>Click me: <img src=x onerror="alert(\'XSS\')"></p>', // After sanitization: <p>Click me: <img src="x" /></p> },};Troubleshooting
Section titled “Troubleshooting”Content is being removed unexpectedly
Section titled “Content is being removed unexpectedly”Check your patterns match the arg path:
// ❌ Wrong: looking for args.content when it's actually args.body.htmlargs: ['content'],
// ✅ Correct:args: ['body.html'],Need to preserve certain HTML elements
Section titled “Need to preserve certain HTML elements”Add them to allowedTags:
sanitizeHtml: { allowedTags: ['details', 'summary'], // Custom elements}Styles are being stripped
Section titled “Styles are being stripped”Configure allowedStyles instead of relying on style attributes:
sanitizeHtml: { allowedStyles: { '*': { color: [/^#/, /^rgb/], // Colors }, },}