Skip to main content
Streamdown applies two layers of HTML security by default, making it safe to render untrusted AI-generated or user-generated markdown.

Default security pipeline

Every markdown string passes through these rehype plugins in order:
  1. rehype-raw — parses raw HTML nodes in the markdown AST so they can be processed
  2. rehype-sanitize — strips dangerous HTML elements and attributes using a schema based on GitHub’s allowlist
  3. rehype-harden — restricts URL protocols, link domains, and image sources
The default configuration is intentionally permissive to allow maximum flexibility:
export const defaultRehypePlugins: Record<string, Pluggable> = {
  raw: rehypeRaw,
  sanitize: [rehypeSanitize, defaultSanitizeSchema],
  harden: [
    harden,
    {
      allowedImagePrefixes: ["*"],  // all images allowed
      allowedLinkPrefixes: ["*"],   // all links allowed
      allowedProtocols: ["*"],      // all protocols allowed
      defaultOrigin: undefined,
      allowDataImages: true,
    },
  ],
};
Tighten this for untrusted or AI-generated content.

rehype-sanitize and defaultSanitizeSchema

Streamdown extends GitHub’s default sanitization schema with:
  • tel: added to allowed href protocols
  • metastring added to allowed <code> attributes (used by the syntax highlighter)
The extended schema is exported:
import { defaultRehypePlugins } from "streamdown";

// The sanitize plugin tuple: [rehypeSanitize, extendedSchema]
console.log(defaultRehypePlugins.sanitize);
All other elements and attributes follow GitHub’s schema — <script>, onclick, style, and similar vectors are stripped.

rehype-harden

rehype-harden runs after sanitization and rewrites or removes URLs that don’t match your allowlists. Override the defaults to restrict which link and image sources are permitted:
import { Streamdown, defaultRehypePlugins } from "streamdown";
import { harden } from "rehype-harden";

<Streamdown
  rehypePlugins={[
    defaultRehypePlugins.raw,
    defaultRehypePlugins.sanitize,
    [
      harden,
      {
        allowedProtocols: ["https", "mailto"],
        allowedLinkPrefixes: ["https://your-app.com", "https://docs.your-app.com"],
        allowedImagePrefixes: ["https://your-cdn.com"],
        allowDataImages: false,
        defaultOrigin: "https://your-app.com",
      },
    ],
  ]}
>
  {markdown}
</Streamdown>
When overriding rehypePlugins, always include defaultRehypePlugins.sanitize to preserve XSS protection. The prop replaces the entire default array — it does not merge.

Protocol restriction

Block javascript:, data:, and other dangerous protocol schemes:
[
  harden,
  {
    allowedProtocols: ["https", "http", "mailto"],
  },
]
To support custom app protocols alongside standard ones:
[
  harden,
  {
    allowedProtocols: ["https", "http", "vscode", "postman", "slack"],
  },
]
Links not matching allowedLinkPrefixes are rewritten to point to defaultOrigin:
[
  harden,
  {
    defaultOrigin: "https://your-app.com",
    allowedLinkPrefixes: [
      "https://your-app.com",
      "https://github.com",
    ],
  },
]

Image source restriction

[
  harden,
  {
    allowedImagePrefixes: ["https://your-cdn.com"],
    allowDataImages: false,  // prevent base64 tracking pixels
  },
]

allowedTags prop

By default, the sanitizer strips unknown HTML tags (while preserving their text content). To allow custom tags like <mention> or <ref>, use the allowedTags prop:
<Streamdown
  allowedTags={{
    mention: ["user_id"],  // allow <mention user_id="..."> through
    ref: ["note_id"],      // allow <ref note_id="..."> through
  }}
  components={{
    mention: ({ user_id }) => <UserBadge id={user_id} />,
    ref: ({ note_id }) => <NoteLink id={note_id} />,
  }}
>
  {markdown}
</Streamdown>
Only attributes explicitly listed in the allowedTags map are preserved — all other attributes on those elements are stripped.
allowedTags only works with the default rehype plugins. If you supply custom rehypePlugins, configure sanitization with your own schema.

literalTagContent prop

Tags listed in literalTagContent have their children treated as plain text — markdown syntax inside them is not parsed. This is useful for mention or entity tags in AI UIs where the child content is a data label:
<Streamdown
  allowedTags={{ mention: ["user_id"] }}
  literalTagContent={["mention"]}
>
  {`<mention user_id="123">@_some_username_</mention>`}
</Streamdown>
Without literalTagContent, @_some_username_ would be parsed as markdown and the underscores would trigger italic formatting. With it, the content renders as-is.
Tags must appear in both allowedTags and literalTagContentliteralTagContent alone does not allow the tag through sanitization.

defaultRehypePlugins and overriding

defaultRehypePlugins is a named-key record so you can surgically replace individual plugins:
import { Streamdown, defaultRehypePlugins } from "streamdown";
import rehypeSanitize from "rehype-sanitize";
import { myStrictSchema } from "./schema";

// Replace only the sanitize plugin, keep raw and harden
<Streamdown
  rehypePlugins={[
    defaultRehypePlugins.raw,
    [rehypeSanitize, myStrictSchema],
    defaultRehypePlugins.harden,
  ]}
>
  {markdown}
</Streamdown>

Disabling raw HTML

To prevent any raw HTML from being interpreted (all tags render as escaped text), omit rehype-raw:
<Streamdown
  rehypePlugins={[
    defaultRehypePlugins.sanitize,
    defaultRehypePlugins.harden,
  ]}
>
  {markdown}
</Streamdown>

Security considerations for AI-generated content

AI models can be manipulated through prompt injection to include malicious links or hidden HTML. A production-ready configuration for AI chat:
import { Streamdown, defaultRehypePlugins } from "streamdown";
import { harden } from "rehype-harden";

export function AIMessage({ content }: { content: string }) {
  return (
    <Streamdown
      linkSafety={{ enabled: true }}
      rehypePlugins={[
        defaultRehypePlugins.raw,
        defaultRehypePlugins.sanitize,
        [
          harden,
          {
            allowedProtocols: ["https", "http", "mailto"],
            allowedLinkPrefixes: [
              "https://your-app.com",
              "https://github.com",
            ],
            allowedImagePrefixes: ["https://your-cdn.com"],
            allowDataImages: false,
            defaultOrigin: "https://your-app.com",
          },
        ],
      ]}
    >
      {content}
    </Streamdown>
  );
}
This configuration:
  • Strips all dangerous HTML via rehype-sanitize
  • Rewrites off-domain links and images via rehype-harden
  • Blocks javascript:, data:, and other protocol injections
  • Shows a confirmation modal before any external navigation via link safety
See Link safety for configuring the confirmation modal.

Build docs developers (and LLMs) love