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.
Key Principle
The package does not include built-in HTML sanitization. This is a deliberate design decision — different applications have different security requirements, and the package provides the hooks and tools for you to implement the appropriate level of protection for your use case.
How Parsing Works
- Markdown parsing is handled by Marked, which tokenizes the markdown into a structured token tree.
- HTML within markdown is parsed by HTMLParser2, which produces structured data rather than directly injecting raw HTML into the DOM.
- Each HTML element is rendered through its own Svelte component, giving you control over what gets rendered and how.
Because HTML is rendered through dedicated Svelte components rather than via {@html} injection, you have a layer of structural safety. Each HTML tag passes through a known component that you can inspect, override, or block.
Blocking HTML in Markdown
The most effective defense is to block HTML tags you do not need. Use the allow/deny filtering utilities:
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:
Using DOMPurify
<script>
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import DOMPurify from 'dompurify'
let rawInput = $state('')
// Sanitize at the source level
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('')
// Sanitize at the source level
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:
- Block all HTML using
buildUnsupportedHTML(). This is the safest approach for untrusted input. - If you need some HTML, use
allowHtmlOnly()with a strict allowlist of safe tags. - Consider sanitizing the raw input before passing it to
SvelteMarkdown.
For trusted content (CMS, documentation, etc.):
- The default renderers are generally safe 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