- 前端
- AI 应用
- UI组件
【免费下载链接】streamdown
A drop-in replacement for react-markdown, designed for AI-powered streaming.
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 流式场景「先出效果、再谈管控」的需求,但并不意味着裸奔——默认安全能力由三层机制提供:
- rehype-sanitize—— XSS 预防,负责剔除危险标签与属性、危险协议,是安全的第一道闸门;
- rehype-harden—— URL/协议限制,默认全部放行(
allowedProtocols: ["*"]、allowedLinkPrefixes: ["*"]、allowedImagePrefixes: ["*"]、allowDataImages: true),但允许你随时收紧; - 外链安全确认弹窗(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 允许的域名前缀白名单 |
allowDataImages | true | 是否允许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.
相关推荐
Streamdown 完全指南:面向 AI 流式渲染的 React Markdown 引擎
Streamdown 完全指南:面向 AI 流式渲染的 React Markdown 引擎 Streamdown 是一款专为 AI 流式输出设计的 React
前端AI 应用UI组件ClawX Markdown 渲染安全与性能规范:Streamdown 流式渲染的架构约束与验证基线
ClawX Markdown 渲染安全与性能规范:Streamdown 流式渲染的架构约束与验证基线 ClawX 将 OpenClaw CLI 的 AI 编排能
人工智能AI 应用桌面应用交互助手为什么选择Streamdown?5大核心优势让AI流式Markdown渲染更高效
为什么选择Streamdown?5大核心优势让AI流式Markdown渲染更高效 Streamdown是一款专为AI驱动的流式渲染设计的Markdown处理工具
前端AI 应用UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考