Files
vercel__streamdown/apps/website/content/docs/security.mdx
Hayden Bleasel e6ab579a89 Add md-metadata
2026-02-11 17:06:45 -08:00

333 lines
8.7 KiB
Plaintext

---
title: Security
description: Built-in content hardening and security features to protect against malicious Markdown.
type: conceptual
summary: Streamdown sanitizes HTML, prevents XSS attacks, and strips dangerous content by default.
related:
- /docs/link-safety
---
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:
1. **[rehype-sanitize](https://github.com/rehypejs/rehype-sanitize)** — strips dangerous HTML elements and attributes using GitHub's sanitization schema, extended with `tel:` protocol support
2. **[rehype-harden](https://github.com/vercel-labs/markdown-sanitizers)** — restricts URL protocols, link domains, and image sources
## Default Security
By default, Streamdown is configured with **permissive security** to allow maximum functionality:
```tsx
// 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:
```tsx
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>
);
}
```
<Callout type="warning">
When overriding `rehypePlugins`, always include `defaultRehypePlugins.sanitize` to preserve XSS protection. The `rehypePlugins` prop **replaces** the entire default array — it does not merge.
</Callout>
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:
```tsx
{
allowedProtocols: [
'http',
'https',
'postman',
'vscode',
'slack',
],
}
```
## Restricting Links
Limit which domains users can link to:
```tsx
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:
```markdown
[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:
```tsx
<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:
```tsx
allowDataImages: false
```
This 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:
```tsx
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:
```tsx
<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](/docs/styling#custom-tags) for more details.
<Callout type="warning">
The `allowedTags` prop only works with the default rehype plugins. If you provide custom `rehypePlugins`, you must configure sanitization yourself.
</Callout>
## HTML Content
Streamdown supports raw HTML through `rehype-raw`. To disable HTML entirely:
```tsx
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:
```tsx
{
defaultOrigin: 'https://your-app.com'
}
```
Relative URLs will be resolved against this origin:
```markdown
[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.
```tsx title="app/page.tsx"
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:
```tsx title="app/page.tsx"
<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.