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 rehype-harden to sanitize and validate content before rendering.

Default Security

By default, Streamdown is configured with permissive security to allow maximum functionality:

// Default configuration
{
  allowedImagePrefixes: ["*"],  // All images allowed
  allowedLinkPrefixes: ["*"],   // All links allowed
  defaultOrigin: undefined,     // No origin restriction
  allowDataImages: true,        // Base64 images allowed
}

This works well for trusted content but should be tightened for untrusted sources.

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.katex,
        [
          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.katex,
    [
      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: 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:

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',
    ],
    allowDataImages: false,
  } : {
    // More permissive for user content
    allowedLinkPrefixes: ['*'],
    allowedImagePrefixes: ['*'],
  };

  return (
    <Streamdown
      rehypePlugins={[
        defaultRehypePlugins.raw,
        defaultRehypePlugins.katex,
        [harden, securityConfig],
      ]}
    >
      {content}
    </Streamdown>
  );
}

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
        defaultRehypePlugins.katex,
        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

Custom URL Transform

For advanced URL handling, use the urlTransform prop:

import { Streamdown, defaultUrlTransform } from 'streamdown';

export default function Page() {
  const customUrlTransform = (url: string) => {
    // First apply default transform
    const transformed = defaultUrlTransform(url);

    // Add your custom logic
    if (transformed.startsWith('http://')) {
      // Upgrade to HTTPS
      return transformed.replace('http://', 'https://');
    }

    // Block specific domains
    if (transformed.includes('untrusted-site.com')) {
      return 'https://your-app.com/blocked';
    }

    return transformed;
  };

  return (
    <Streamdown urlTransform={customUrlTransform}>
      {markdown}
    </Streamdown>
  );
}