Security
Built-in content hardening and security features to protect against malicious Markdown.
Streamdown is built with security as a top priority. When rendering user-generated or AI-generated Markdown content, it's crucial to protect against malicious content, especially when dealing with content that might have been subject to prompt injection attacks.
Why Security Matters
Markdown can contain:
- Links to malicious sites - Phishing or malware distribution
- External images - Privacy tracking or CSRF attacks
- HTML content - XSS vulnerabilities
- JavaScript execution - Code injection
- Prompt injection - AI models manipulated to include harmful content
Streamdown uses two layers of protection:
- rehype-sanitize — strips dangerous HTML elements and attributes using GitHub's sanitization schema, extended with
tel:protocol support - rehype-harden — restricts URL protocols, link domains, and image sources
Default Security
By default, Streamdown is configured with permissive security to allow maximum functionality:
// Default rehype-harden configuration
{
allowedImagePrefixes: ["*"], // All images allowed
allowedLinkPrefixes: ["*"], // All links allowed
allowedProtocols: ["*"], // All protocols allowed
defaultOrigin: undefined, // No origin restriction
allowDataImages: true, // Base64 images allowed
}The default rehype-sanitize schema allows http, https, irc, ircs, mailto, xmpp, and tel protocols for links.
This works well for trusted content but should be tightened for untrusted sources.
Restricting Protocols
By default, all protocols are allowed. You can restrict which URL protocols are permitted:
import { Streamdown, defaultRehypePlugins } from 'streamdown';
import { harden } from 'rehype-harden';
export default function Page() {
return (
<Streamdown
rehypePlugins={[
defaultRehypePlugins.raw,
defaultRehypePlugins.sanitize,
[
harden,
{
allowedProtocols: [
'http',
'https',
'mailto',
],
},
],
]}
>
{markdown}
</Streamdown>
);
}When overriding rehypePlugins, always include defaultRehypePlugins.sanitize to preserve XSS protection. The rehypePlugins prop replaces the entire default array — it does not merge.
This is useful for security-sensitive applications where you want to prevent custom protocol schemes like javascript:, data:, or desktop app protocols.
Custom Protocol Schemes
To enable custom protocol schemes like postman://, vscode://, or slack://, include them in the allowedProtocols array:
{
allowedProtocols: [
'http',
'https',
'postman',
'vscode',
'slack',
],
}Restricting Links
Limit which domains users can link to:
import { Streamdown, defaultRehypePlugins } from 'streamdown';
import { harden } from 'rehype-harden';
export default function Page() {
return (
<Streamdown
rehypePlugins={[
defaultRehypePlugins.raw,
defaultRehypePlugins.sanitize,
[
harden,
{
defaultOrigin: 'https://streamdown.ai',
allowedLinkPrefixes: [
'https://streamdown.ai',
'https://github.com',
'https://vercel.com',
],
},
],
]}
>
{markdown}
</Streamdown>
);
}Any links not matching the allowed prefixes will be rewritten to point to the defaultOrigin.
Example
With the above configuration:
[Safe link](https://github.com/vercel/streamdown)
[Unsafe link](https://malicious-site.com)Results in:
- Safe link: Works normally
- Unsafe link: Renders as [blocked]
Restricting Images
Similarly, restrict which domains can serve images:
<Streamdown
rehypePlugins={[
defaultRehypePlugins.raw,
defaultRehypePlugins.sanitize,
[
harden,
{
allowedImagePrefixes: [
'https://your-cdn.com',
'https://trusted-images.com',
],
allowDataImages: false, // Disable base64 images
},
],
]}
>
{markdown}
</Streamdown>Data Images
Base64-encoded images (data:image/...) can be disabled:
allowDataImages: falseThis prevents embedding arbitrary image data in Markdown, which could be used for:
- Tracking pixels
- Large embedded files
- Malicious payloads
Protecting Against Prompt Injection
When using AI-generated content, models can be manipulated to include malicious links or content. Here's a production-ready configuration:
import { Streamdown, defaultRehypePlugins } from 'streamdown';
import { harden } from 'rehype-harden';
export default function ChatMessage({ content, isAIGenerated }) {
const securityConfig = isAIGenerated ? {
defaultOrigin: 'https://your-app.com',
allowedLinkPrefixes: [
'https://your-app.com',
'https://docs.your-app.com',
'https://github.com',
],
allowedImagePrefixes: [
'https://your-cdn.com',
],
allowedProtocols: [
'http',
'https',
'mailto',
],
allowDataImages: false,
} : {
// More permissive for user content
allowedLinkPrefixes: ['*'],
allowedImagePrefixes: ['*'],
allowedProtocols: ['*'],
};
return (
<Streamdown
rehypePlugins={[
defaultRehypePlugins.raw,
defaultRehypePlugins.sanitize,
[harden, securityConfig],
]}
>
{content}
</Streamdown>
);
}Custom HTML Tags
By default, Streamdown's sanitizer strips unknown HTML tags (while preserving their content). If you need to render custom tags like <ref> or <mention>, use the allowedTags prop:
<Streamdown
allowedTags={{
ref: ["note_id"], // Allow <ref> with note_id attribute
mention: ["user_id"], // Allow <mention> with user_id attribute
}}
components={{
ref: (props) => <NoteBadge noteId={props.note_id} />,
mention: (props) => <UserMention userId={props.user_id} />,
}}
>
{markdown}
</Streamdown>Only attributes explicitly listed in allowedTags are preserved—all other attributes are stripped for security. See the Styling documentation for more details.
The allowedTags prop only works with the default rehype plugins. If you provide custom rehypePlugins, you must configure sanitization yourself.
HTML Content
Streamdown supports raw HTML through rehype-raw. To disable HTML entirely:
import { Streamdown, defaultRehypePlugins } from 'streamdown';
export default function Page() {
return (
<Streamdown
rehypePlugins={[
// Omit defaultRehypePlugins.raw to disable HTML
defaultRehypePlugins.sanitize,
defaultRehypePlugins.harden,
]}
>
{markdown}
</Streamdown>
);
}Without rehype-raw, HTML tags will be escaped and displayed as text.
Relative URLs
Control how relative URLs are handled:
{
defaultOrigin: 'https://your-app.com'
}Relative URLs will be resolved against this origin:
[Relative link](/docs/guide)Becomes: https://your-app.com/docs/guide
URL Transform
For URL-level control without writing a rehype plugin, use the urlTransform prop. This runs on every URL in the rendered Markdown (links, images, etc.) and matches the react-markdown API.
By default, defaultUrlTransform is a passthrough — URL security is already handled by rehype-sanitize and rehype-harden. Use urlTransform when you need custom URL rewriting beyond what those plugins provide.
import { Streamdown, defaultUrlTransform } from 'streamdown';
// Proxy images through your CDN
<Streamdown
urlTransform={(url, key, node) => {
if (key === 'src') {
return `https://your-cdn.com/proxy?url=${encodeURIComponent(url)}`;
}
return defaultUrlTransform(url, key, node);
}}
>
{markdown}
</Streamdown>Skipping HTML
To completely ignore raw HTML in Markdown (rather than escaping it), use the skipHtml prop:
<Streamdown skipHtml>
{markdown}
</Streamdown>Advanced URL Handling
For advanced URL handling beyond what urlTransform and rehype-harden provide, you can create a custom rehype plugin. This gives you full control over URL transformation and validation in your markdown content.