☰
Streamdown 安全指南:为 AI 流式 Markdown 渲染构建可靠的安全边界
2026/10/12 7:48:49 网站建设 项目流程
  • 前端
  • AI 应用
  • UI组件

【免费下载链接】streamdown

A drop-in replacement for react-markdown, designed for AI-powered streaming.

项目地址:https://gitcode.com/gh_mirrors/stre/streamdown
点击查看免费下载

Streamdown 是一款面向 AI 流式输出的 react-markdown 替代方案。本文以其安全参考文档为主体,结合仓库源码(packages/streamdown/index.tsx、packages/streamdown/lib/link-modal.tsx、packages/streamdown/lib/components.tsx等)展开,系统讲解 Streamdown 的默认安全姿态、协议与链接/图片白名单、外链安全确认弹窗、自定义 HTML 标签白名单、URL 改写与生产级严格配置,帮助你针对不可信的 AI 生成内容搭建层层防御。

默认安全姿态:宽松渲染,但三层防线护航

Streamdown 的默认行为是宽松的(permissive):所有 URL 前缀、所有协议、data: 图片均被放行。这符合 AI 流式场景「先出效果、再谈管控」的需求,但并不意味着裸奔——默认安全能力由三层机制提供:

  1. rehype-sanitize—— XSS 预防,负责剔除危险标签与属性、危险协议,是安全的第一道闸门;
  2. rehype-harden—— URL/协议限制,默认全部放行(allowedProtocols: ["*"]、allowedLinkPrefixes: ["*"]、allowedImagePrefixes: ["*"]、allowDataImages: true),但允许你随时收紧;
  3. 外链安全确认弹窗(Link Safety Modal)—— 打开外部链接前弹出确认框,默认启用。

这些默认值可以在源码中直接验证:defaultRehypePlugins定义于packages/streamdown/index.tsx,其中 harden 插件的默认配置为:

export const defaultRehypePlugins: Record<string, Pluggable> = { raw: rehypeRaw, sanitize: [rehypeSanitize, defaultSanitizeSchema], harden: [ harden, { allowedImagePrefixes: ["*"], allowedLinkPrefixes: ["*"], allowedProtocols: ["*"], defaultOrigin: undefined, allowDataImages: true, }, ], };

同时,默认的 sanitize schema 也做了针对流式场景的定制(defaultSanitizeSchema,见index.tsx):

  • clobberPrefix: ""—— 避免 remark-rehype 与 hast-util-sanitize 双重加user-content-前缀导致脚注锚点 id 错位;
  • 在默认协议基础上追加tel与streamdown—— 其中streamdown:是 remend 在流式渲染过程中为"未完成链接/图片"注入的哨兵协议(streamdown:incomplete-link/incomplete-image);
  • code元素额外放行metastring属性,支撑代码块元信息(如startLine)的传递。

结论:默认配置足够安全地渲染静态或流式 Markdown;一旦你的内容来自 AI 或第三方用户输入,就应当按下文逐步收紧配置。

限制协议:从「全放行」到「白名单」

rehype-harden 的allowedProtocols控制链接与图片可用的 URL 协议。默认是["*"],即全部放行。收紧到仅允许https与mailto:

<Streamdown rehypePlugins={[ rehypeRaw, [rehypeSanitize, {}], [harden, { allowedProtocols: ['https', 'mailto'], allowedLinkPrefixes: ['*'], allowedImagePrefixes: ['*'], allowDataImages: true, }], ]} >

配置要点:

参数默认值作用
allowedProtocols["*"]允许的 URL 协议白名单,如https、mailto、tel
allowedLinkPrefixes["*"]链接 href 允许的域名前缀白名单
allowedImagePrefixes["*"]图片 src 允许的域名前缀白名单
allowDataImagestrue是否允许data:协议的图片(base64 内嵌图片)

rehype-sanitize同样在协议层做兜底校验。需要注意的是:自定义rehypePlugins会替换默认插件数组(而不是追加),因此一旦你传入自定义数组,就应把rehypeRaw、rehypeSanitize、harden三层完整补齐,否则会出现"开了 raw 却没消毒"的安全缺口。

限制链接:只允许可信域名

当 AI 生成的内容可能夹带钓鱼或追踪链接时,把allowedLinkPrefixes收窄到企业自有域名即可:

[harden, { allowedLinkPrefixes: [ 'https://example.com', 'https://docs.example.com', ], allowedProtocols: ['https'], }]

前缀匹配是逐段包含的,https://example.com可以放行https://example.com/foo及子路径,但不会放行https://example.com.evil.com这类变体域名(因为前缀https://example.com与https://example.com.evil.com并不构成连续前缀)。如需更精确的域名级判断,可结合下文linkSafety.onLinkCheck做二次校验。

限制图片:域名白名单与禁用 data: 图片

图片是 XSS 与隐私追踪的常见载体(例如通过src触发远程请求、通过data:内嵌恶意 HTML 渲染上下文)。收紧方式:

[harden, { allowedImagePrefixes: [ 'https://images.example.com', 'https://cdn.example.com', ], allowDataImages: false, // 禁用 data: URL }]

allowDataImages: false会拒绝 base64 内嵌图片。这对流式场景的影响是:若你的 AI 输出依赖data:图片(如部分 LLM 直接生成 SVG 或 base64 位图),需权衡是否放行;生产环境建议默认关闭。

Link Safety Modal:打开外链前的确认闸门

默认行为

Link Safety Modal默认启用(defaultLinkSafetyConfig = { enabled: true },见index.tsx)。当渲染的链接开启该功能时,<a>会被渲染成<button>(带data-streamdown="link"标记),点击后拦截默认跳转,先弹出确认框,用户点击"继续"才通过window.open(href, "_blank", "noreferrer")打开(见LinkComponent)。

默认弹窗(LinkSafetyModal)具备以下行为:

  • 展示目标 URL(超长 URL 自动限高滚动,break-all防溢出);
  • 提供「复制链接」与「打开链接」按钮;
  • 点击遮罩或按Esc关闭;
  • 打开时锁定页面滚动(lockBodyScroll),关闭时释放;
  • 通过createPortal渲染到document.body(或portalprop 指定的容器),避免被父级样式/层级干扰。

测试用例验证了该流程:link-safety.test.tsx中「点击链接 → 弹窗出现 → 展示 URL」的断言覆盖了默认拦截路径。

完全禁用

<Streamdown linkSafety={{ enabled: false }}>{markdown}</Streamdown>

禁用后链接恢复为原生<a>行为,点击直接跳转。

白名单可信域名(onLinkCheck)

onLinkCheck是异步(或同步)返回布尔值的校验函数:返回true时跳过弹窗、直接window.open;返回false时弹出确认框。基于 hostname 后缀匹配的常见实现:

<Streamdown linkSafety={{ enabled: true, onLinkCheck: async (url) => { const trusted = ['example.com', 'docs.example.com']; const hostname = new URL(url).hostname; return trusted.some((d) => hostname.endsWith(d)); }, }} >

测试覆盖了onLinkCheck返回true时「不弹窗、直接window.open(..., "_blank", "noreferrer")」的行为。这里有一个值得注意的安全细节:endsWith(d)会匹配example.com与sub.example.com,这是期望的层级域名匹配;若想杜绝notexample.com这类子串欺诈,可先取 hostname 再比对完整域名或使用hostname === d || hostname.endsWith('.' + d)。

自定义弹窗(renderModal)

弹窗 UI 完全可替换。renderModal接收{ url, isOpen, onClose, onConfirm }四个参数,你可以接任意 UI 库:

<Streamdown linkSafety={{ enabled: true, renderModal: ({ url, isOpen, onClose, onConfirm }) => ( <Dialog open={isOpen} onOpenChange={onClose}> <DialogContent> <p>Open {url}?</p> <Button onClick={onConfirm}>Continue</Button> <Button onClick={onClose}>Cancel</Button> </DialogContent> </Dialog> ), }} >

LinkSafetyModalProps的类型定义({ isOpen; onClose; onConfirm; url })见index.tsx,与自定义组件签名完全对齐。图标、文案还可用icons与translationsprop 覆盖。

自定义 HTML 标签白名单:安全承载 AI 生成的富组件

AI 输出里常出现<mention>、<widget>、<source>这类语义化自定义标签。默认 sanitize schema 会将其剥离,需要通过allowedTags显式白名单,并在components中提供对应渲染组件:

<Streamdown allowedTags={{ source: ["id"], mention: ["user_id", "type"], widget: ["data*"], // 通配符:放行所有>import { Streamdown, defaultUrlTransform } from 'streamdown'; // 通过你的 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>

签名(url, key, node)中:url是原始 URL,key是属性名(如href/src),node是当前的 hast 元素。实现层面,transformUrls遍历html-url-attributes定义的 URL 属性表,只对匹配当前标签的属性调用 transform。注意:urlTransform返回undefined时该属性会被置空,不要误返回空值导致链接丢失。

跳过与禁用 HTML

skipHtml:完全忽略原始 HTML

skipHtml会丢弃 Markdown 中的所有原始 HTML 标签(以纯文本或直接删除的方式处理),适用于"只要 Markdown 语义、绝不渲染任何 HTML"的严格场景:

<Streamdown skipHtml>{markdown}</Streamdown>

禁用 HTML:移除 rehype-raw

rehype-raw负责把 Markdown 中的原始 HTML 解析为真实 DOM 节点。去掉它,所有原始 HTML 都会被当作纯文本输出(等价于"看不见 HTML 语法")。由于默认插件是打包导出的,可以这样移除:

import { defaultRehypePlugins } from 'streamdown'; const { raw, ...rest } = defaultRehypePlugins; <Streamdown rehypePlugins={Object.values(rest)}>{markdown}</Streamdown>

defaultRehypePlugins包含raw、sanitize、harden三个键(见index.tsx),去掉raw后剩下的sanitize+harden仍会执行,因此这条路比skipHtml更温和:HTML 变成字面文本展示给用户,而不是被静默丢弃。

生产级严格配置:AI 生成内容的完整安全示例

综合以上全部手段,针对 AI 生成内容(aiContent)的严格生产配置如下:

<Streamdown rehypePlugins={[ rehypeRaw, [rehypeSanitize, {}], [harden, { allowedProtocols: ['https', 'mailto'], allowedLinkPrefixes: ['https://your-domain.com'], allowedImagePrefixes: ['https://your-cdn.com'], allowDataImages: false, }], ]} linkSafety={{ enabled: true, onLinkCheck: async (url) => { const trusted = ['your-domain.com']; return trusted.some((d) => new URL(url).hostname.endsWith(d)); }, }} > {aiContent} </Streamdown>

该配置对应三层防线:

防线配置效果
sanitize 层rehypeSanitize剥离危险标签/属性/协议,兜底 XSS
harden 层allowedProtocols+allowedLinkPrefixes+allowedImagePrefixes+allowDataImages: false链接只指向自家域名、图片只来自自家 CDN、禁用 data: 图片
交互层linkSafety.onLinkCheck非白名单域名一律弹窗确认,杜绝无感知跳转

补充建议(基于源码与仓库实践)

  • 代码块与表格元数据:如需放行代码块metastring、表格等增强能力,请保留默认 sanitize schema 的扩展项(packages/streamdown/index.tsx的defaultSanitizeSchema已内置),自定义 schema 时勿删减streamdown:哨兵协议,否则流式渲染中未完成的链接/图片会因协议被拒而闪烁。
  • 尾部 URL 探测:如需彻底收紧自动链接(如裸邮箱被 GFM 自动转为mailto:),可使用disableAutolinkProtocols={["mailto"]}prop,它会在 remarkGfm 之后运行remarkDisableAutolinkProtocols剔除指定协议的自动链接节点(index.tsx)。
  • 不要绕过 sanitize:rehypeRaw只负责解析、不负责安全,任何自定义插件数组都必须显式携带 sanitize/harden,切勿只传rehypeRaw。

结语

Streamdown 的安全体系遵循「默认宽松、三层兜底、按需收紧」的设计:rehype-sanitize负责 XSS 预防,rehype-harden负责 URL/协议白名单,linkSafety负责交互层的外链确认,allowedTags为 AI 自定义标签提供受控的渲染空间。结合packages/streamdown/__tests__下的测试用例(link-safety.test.tsx、allowed-tags.test.tsx等)可以验证上述每个安全行为。在生产环境中面向 AI 输出时,请始终以"内容不可信"为前提,将协议、域名、data: 图片逐项收紧,并保留外链确认这道人机交互的最后防线。

  • 前端
  • AI 应用
  • UI组件

【免费下载链接】streamdown

A drop-in replacement for react-markdown, designed for AI-powered streaming.

项目地址:https://gitcode.com/gh_mirrors/stre/streamdown
点击查看免费下载
上一篇:5分钟掌握Spek:免费开源音频频谱分析器的专业指南
下一篇:免费开源的音频频谱分析神器:Spek终极使用指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询