Security
@humanspeak/svelte-markdown renders markdown content into HTML. When processing user-generated markdown, it is important to understand the security implications and how to protect against cross-site scripting (XSS) and other injection attacks.
Built-in URL and Attribute Sanitization
The library includes built-in sanitization that runs in the Parser before tokens reach any renderer component or snippet. This means custom renderers and Svelte 5 snippets cannot bypass it.
URL Sanitization
By default, all href values on markdown links and images are validated against a protocol allowlist. Only safe protocols are permitted:
http:andhttps:mailto:andtel:- Relative URLs (
/path,#anchor,?query,./relative)
Dangerous protocols like javascript:, data:, vbscript:, and blob: are blocked automatically.
<!-- This link will have its href stripped -->
[Click me](javascript:alert('XSS'))
<!-- This link renders normally -->
[Click me](https://example.com)<!-- This link will have its href stripped -->
[Click me](javascript:alert('XSS'))
<!-- This link renders normally -->
[Click me](https://example.com)HTML Attribute Sanitization
For raw HTML within markdown, the default sanitizer:
- Strips all event handler attributes (
onclick,onerror,onload, etc.) - Validates URL-bearing attributes (
href,src,action,formaction,cite,data,poster) against the same protocol allowlist
<!-- onclick is stripped, javascript: href is blocked -->
<a href="javascript:alert('XSS')" onclick="alert('XSS')">Click</a>
<!-- Safe attributes are preserved -->
<a href="https://example.com" class="link">Click</a><!-- onclick is stripped, javascript: href is blocked -->
<a href="javascript:alert('XSS')" onclick="alert('XSS')">Click</a>
<!-- Safe attributes are preserved -->
<a href="https://example.com" class="link">Click</a>Context-Aware Overrides
Both sanitization functions receive a context object with the token type and HTML tag name, so you can implement per-element policies:
<script>
import SvelteMarkdown, { defaultSanitizeUrl } from '@humanspeak/svelte-markdown'
// Allow data: URIs only on images, block everywhere else
function customSanitizeUrl(url, { type, tag }) {
if (tag === 'img' && url.startsWith('data:image/')) return url
return defaultSanitizeUrl(url, { type, tag })
}
</script>
<SvelteMarkdown source={markdown} sanitizeUrl={customSanitizeUrl} /><script>
import SvelteMarkdown, { defaultSanitizeUrl } from '@humanspeak/svelte-markdown'
// Allow data: URIs only on images, block everywhere else
function customSanitizeUrl(url, { type, tag }) {
if (tag === 'img' && url.startsWith('data:image/')) return url
return defaultSanitizeUrl(url, { type, tag })
}
</script>
<SvelteMarkdown source={markdown} sanitizeUrl={customSanitizeUrl} />Custom Attribute Sanitization
Override sanitizeAttributes to apply per-tag attribute rules:
<script>
import SvelteMarkdown, {
defaultSanitizeAttributes,
defaultSanitizeUrl
} from '@humanspeak/svelte-markdown'
// Block all attributes on iframes, use defaults for everything else
function customSanitizeAttributes(attributes, context, sanitizeUrl) {
if (context.tag === 'iframe') return {}
return defaultSanitizeAttributes(attributes, context, sanitizeUrl)
}
</script>
<SvelteMarkdown
source={markdown}
sanitizeAttributes={customSanitizeAttributes}
/><script>
import SvelteMarkdown, {
defaultSanitizeAttributes,
defaultSanitizeUrl
} from '@humanspeak/svelte-markdown'
// Block all attributes on iframes, use defaults for everything else
function customSanitizeAttributes(attributes, context, sanitizeUrl) {
if (context.tag === 'iframe') return {}
return defaultSanitizeAttributes(attributes, context, sanitizeUrl)
}
</script>
<SvelteMarkdown
source={markdown}
sanitizeAttributes={customSanitizeAttributes}
/>Disabling Sanitization
If you fully trust your markdown source and need to allow all URLs and attributes, use the passthrough functions:
<script>
import SvelteMarkdown, {
unsanitizedUrl,
unsanitizedAttributes
} from '@humanspeak/svelte-markdown'
</script>
<SvelteMarkdown
source={trustedContent}
sanitizeUrl={unsanitizedUrl}
sanitizeAttributes={unsanitizedAttributes}
/><script>
import SvelteMarkdown, {
unsanitizedUrl,
unsanitizedAttributes
} from '@humanspeak/svelte-markdown'
</script>
<SvelteMarkdown
source={trustedContent}
sanitizeUrl={unsanitizedUrl}
sanitizeAttributes={unsanitizedAttributes}
/>Warning: Only disable sanitization for content you fully control. Never use passthrough functions with user-generated input.
Sanitization API Reference
| Export | Type | Description |
|---|---|---|
defaultSanitizeUrl | SanitizeUrlFn | Protocol allowlist (http, https, mailto, tel, relative) |
defaultSanitizeAttributes | SanitizeAttributesFn | Strips on* handlers, validates URL attributes |
unsanitizedUrl | SanitizeUrlFn | Passthrough — allows all URLs |
unsanitizedAttributes | SanitizeAttributesFn | Passthrough — allows all attributes |
SanitizeUrlFn | Type | (url: string, context: SanitizeContext) => string |
SanitizeAttributesFn | Type | (attributes: Record<string, string>, context: SanitizeContext, sanitizeUrl: SanitizeUrlFn) => Record<string, string> |
SanitizeContext | Type | { type: string, tag: string } |
Blocking HTML in Markdown
For additional defense, you can block HTML tags entirely using the allow/deny filtering utilities. This is complementary to URL/attribute sanitization.
Block All HTML
<script>
import SvelteMarkdown, { buildUnsupportedHTML } from '@humanspeak/svelte-markdown'
const renderers = {
html: buildUnsupportedHTML()
}
</script>
<SvelteMarkdown source={userInput} {renderers} /><script>
import SvelteMarkdown, { buildUnsupportedHTML } from '@humanspeak/svelte-markdown'
const renderers = {
html: buildUnsupportedHTML()
}
</script>
<SvelteMarkdown source={userInput} {renderers} />This replaces every HTML tag renderer with UnsupportedHTML, which renders nothing. Markdown syntax (headings, lists, emphasis, etc.) still works normally.
Allow Only Safe Tags
<script>
import SvelteMarkdown, { allowHtmlOnly } from '@humanspeak/svelte-markdown'
const safeHtml = allowHtmlOnly([
'strong', 'em', 'a', 'code', 'pre',
'ul', 'ol', 'li', 'p', 'br', 'hr',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'blockquote', 'table', 'thead', 'tbody',
'tr', 'th', 'td'
])
</script>
<SvelteMarkdown source={userInput} renderers={{ html: safeHtml }} /><script>
import SvelteMarkdown, { allowHtmlOnly } from '@humanspeak/svelte-markdown'
const safeHtml = allowHtmlOnly([
'strong', 'em', 'a', 'code', 'pre',
'ul', 'ol', 'li', 'p', 'br', 'hr',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'blockquote', 'table', 'thead', 'tbody',
'tr', 'th', 'td'
])
</script>
<SvelteMarkdown source={userInput} renderers={{ html: safeHtml }} />Block Specific Dangerous Tags
<script>
import SvelteMarkdown, { excludeHtmlOnly } from '@humanspeak/svelte-markdown'
const filteredHtml = excludeHtmlOnly([
'iframe', 'embed', 'form', 'input',
'textarea', 'select', 'button',
'canvas', 'dialog'
])
</script>
<SvelteMarkdown source={userInput} renderers={{ html: filteredHtml }} /><script>
import SvelteMarkdown, { excludeHtmlOnly } from '@humanspeak/svelte-markdown'
const filteredHtml = excludeHtmlOnly([
'iframe', 'embed', 'form', 'input',
'textarea', 'select', 'button',
'canvas', 'dialog'
])
</script>
<SvelteMarkdown source={userInput} renderers={{ html: filteredHtml }} />Restricting Markdown Elements
You can also restrict which markdown elements are rendered:
<script>
import SvelteMarkdown, {
allowRenderersOnly,
buildUnsupportedHTML
} from '@humanspeak/svelte-markdown'
// Only allow basic text formatting
const renderers = {
...allowRenderersOnly([
'paragraph', 'text', 'strong', 'em',
'link', 'list', 'listitem', 'br'
]),
html: buildUnsupportedHTML()
}
</script>
<SvelteMarkdown source={userInput} {renderers} /><script>
import SvelteMarkdown, {
allowRenderersOnly,
buildUnsupportedHTML
} from '@humanspeak/svelte-markdown'
// Only allow basic text formatting
const renderers = {
...allowRenderersOnly([
'paragraph', 'text', 'strong', 'em',
'link', 'list', 'listitem', 'br'
]),
html: buildUnsupportedHTML()
}
</script>
<SvelteMarkdown source={userInput} {renderers} />External Sanitization
For applications that require additional protection, you can sanitize the markdown source before passing it to SvelteMarkdown. Note that input-level sanitizers like DOMPurify operate on HTML, not Markdown syntax — they will not catch Markdown-native injection vectors like [Click](javascript:alert('XSS')). The built-in URL sanitization handles this automatically.
Using DOMPurify (Combined with Built-in Sanitization)
<script>
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import DOMPurify from 'dompurify'
let rawInput = $state('')
// DOMPurify handles HTML-level threats
// Built-in sanitization handles Markdown-native threats
const sanitized = $derived(
DOMPurify.sanitize(rawInput, { ALLOWED_TAGS: [] })
)
</script>
<SvelteMarkdown source={sanitized} /><script>
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import DOMPurify from 'dompurify'
let rawInput = $state('')
// DOMPurify handles HTML-level threats
// Built-in sanitization handles Markdown-native threats
const sanitized = $derived(
DOMPurify.sanitize(rawInput, { ALLOWED_TAGS: [] })
)
</script>
<SvelteMarkdown source={sanitized} />Using the parsed Callback
You can inspect and filter tokens after parsing:
<script>
import SvelteMarkdown from '@humanspeak/svelte-markdown'
function handleParsed(tokens) {
// Inspect tokens for suspicious content
const htmlTokens = tokens.filter(t => t.type === 'html')
if (htmlTokens.length > 0) {
console.warn('HTML detected in markdown input:', htmlTokens)
}
}
</script>
<SvelteMarkdown source={userInput} parsed={handleParsed} /><script>
import SvelteMarkdown from '@humanspeak/svelte-markdown'
function handleParsed(tokens) {
// Inspect tokens for suspicious content
const htmlTokens = tokens.filter(t => t.type === 'html')
if (htmlTokens.length > 0) {
console.warn('HTML detected in markdown input:', htmlTokens)
}
}
</script>
<SvelteMarkdown source={userInput} parsed={handleParsed} />Security Recommendations
For user-generated content:
- Use the built-in sanitization (enabled by default). This blocks dangerous URL protocols and event handler attributes.
- Block all HTML using
buildUnsupportedHTML()for the strictest protection. - If you need some HTML, use
allowHtmlOnly()with a strict allowlist of safe tags. - Consider combining with DOMPurify for defense-in-depth.
For trusted content (CMS, documentation, etc.):
- The default sanitization is generally sufficient for trusted sources.
- Use
excludeHtmlOnly()to block specific tags you do not want (e.g.,iframe,form).
General best practices:
- Validate input on the server. Client-side filtering adds defense-in-depth but should not be your only protection.
- Use Content Security Policy (CSP) headers. Restrict inline scripts and other dangerous behaviors at the browser level.
- Keep dependencies updated. Regularly update
@humanspeak/svelte-markdown, Marked, and HTMLParser2 to get security fixes. - Audit your custom renderers. If you create custom renderers, make sure they do not use
{@html}with untrusted content.
What About <script> Tags?
Marked does not produce <script> tokens by default, and HTMLParser2 processes HTML structurally rather than executing it. Additionally, the HTML renderer map does not include a script component, so <script> tags in markdown are not rendered.
However, for defense-in-depth, you should still block or sanitize HTML when processing untrusted input.
Related
- Allow/Deny Filtering — detailed filtering function documentation
- HTML Renderers — all HTML tag renderers
- HTML Filtering Examples — practical filtering examples