把 AI 生成的内容渲染到页面上,有哪些安全风险?

进阶高频安全实践约 7 分钟读完

一句话回答

模型输出是不可信内容:它可能被提示注入操纵,也可能原样复述用户输入或网页里的恶意代码。直接用 v-html 或 dangerouslySetInnerHTML 渲染会导致 XSS。Markdown 渲染器要关闭原始 HTML,或者渲染后用 DOMPurify 清洗;链接要过滤 javascript: 等危险协议;外链图片在渲染时会自动发出请求,可能被用来外传数据,要禁用或只允许白名单域名;最后用 CSP 兜底。

详细解析

为什么模型输出不可信

  • 模型会复述输入:用户粘贴的代码、检索到的网页里有恶意 HTML,模型可能原样带到回答里
  • 模型可能被间接提示注入操纵,故意输出攻击内容
  • 即使没有攻击,模型也会自己写出 HTML 标签,这些标签不该被浏览器执行

所以模型输出要和用户输入一样对待。XSS 的基础原理见 XSS 和 CSRF。

风险和防御

风险 怎么发生 防御
HTML 注入 渲染了带 <img onerror> 或 <script> 的内容 关闭 Markdown 的原始 HTML;必须保留时用 DOMPurify 清洗
危险协议的链接 链接地址以 javascript: 开头,用户一点就执行脚本 只允许 http:、https:、mailto: 等协议
外链图片外传数据 图片地址带着对话里的敏感数据,浏览器加载时自动发出请求 不渲染外部图片,或只允许白名单域名;CSP 的 img-src 兜底
钓鱼链接 链接文字写着"官方文档",实际指向仿冒网站 显示真实域名;跳转外链前提示
输出进入其他系统 模型输出被拼进 SQL、shell 命令、邮件模板 同样当作不可信输入:参数化查询、转义、白名单校验

常见 Markdown 库的默认行为

库 原始 HTML 危险协议的链接
markdown-it 默认 html: false,HTML 被转义成文本 默认拦截 javascript:、vbscript:、file: 和大部分 data:
marked 默认原样输出,官方建议对结果用 DOMPurify 等库清洗 同样交给清洗库处理
react-markdown 默认转义 HTML,加了 rehype-raw 才会渲染 默认只放行 http、https、mailto 等少数协议

这些库的默认配置都不处理外链图片,要自己加规则。流式输出时每次重新渲染都要走同样的清洗流程,不能为了性能跳过,流式渲染的其他问题见流式 Markdown 渲染。

代码示例

markdown-it:关闭 HTML,外链图片只允许白名单域名,最后再用 DOMPurify 清洗一遍:

TypeScript
import MarkdownIt from 'markdown-it'
import DOMPurify from 'dompurify'

const IMAGE_HOSTS = new Set(['cdn.example.com']) // 允许加载的图片域名

const md = new MarkdownIt({ html: false, linkify: true })

const renderImage = md.renderer.rules.image!
md.renderer.rules.image = (tokens, idx, options, env, self) => {
  const token = tokens[idx]
  let host = ''
  try {
    host = new URL(token.attrGet('src') ?? '').hostname
  } catch {
    // 相对路径或非法地址,按不允许处理
  }
  if (!IMAGE_HOSTS.has(host)) {
    // 不加载图片,只显示替代文字
    return md.utils.escapeHtml(`[图片:${token.content || '无描述'}]`)
  }
  return renderImage(tokens, idx, options, env, self)
}

// 第二道保险:以后有人打开了 html 选项、或者加了直接输出 HTML 的插件,也不会出问题
export const renderAnswer = (text: string) => DOMPurify.sanitize(md.render(text))

Vue 组件里用 v-html 绑定 renderAnswer(content) 的结果即可:问题从来不在 v-html 本身,而在于绑定的内容有没有清洗。

React 项目用 react-markdown 时,不要给模型输出加 rehype-raw,图片通过 components 覆盖(isAllowedImage 按域名白名单判断):

TSX
import Markdown from 'react-markdown'

export function Answer({ content }: { content: string }) {
  return (
    <Markdown
      components={{
        img: ({ src, alt }) =>
          isAllowedImage(src) ? <img src={src} alt={alt} /> : <span>[图片:{alt}]</span>,
      }}
    >
      {content}
    </Markdown>
  )
}

最后在响应头里用 CSP 限制图片来源,前面的规则有遗漏时,浏览器也不会去请求:

文本
Content-Security-Policy: img-src 'self' https://cdn.example.com

面试官可能追问

markdown-it 已经关闭了 HTML,为什么还要 DOMPurify?

纵深防御。配置可能被后来的人改掉,有些插件(数学公式、自定义容器)会直接输出 HTML,不受 html: false 控制。DOMPurify 的成本很低,放在最后一步能兜住这些情况。服务端渲染时没有 DOM,需要配合 jsdom 使用。

模型生成的 HTML 页面或代码,想在前端预览运行怎么办?

不要在主页面里执行。放进带 sandbox 属性的 iframe,并且用独立的域名提供,让它和主站不同源,读不到主站的 Cookie、localStorage 和 DOM;再用 CSP 限制它能访问的网络地址。更多隔离手段见代码执行沙箱。

为什么外链图片比链接危险得多?

图片在渲染时由浏览器自动请求,用户什么都不用做,数据就发出去了;链接需要用户点击,用户还有机会看到真实地址。不过有的应用会自动抓取链接生成预览卡片,这时链接也变成了自动请求,要一并处理。

模型输出的图表配置里带了函数,能用 eval 执行吗?

不能。让模型只输出 JSON 格式的配置,用 Schema 校验后再交给图表库;需要的格式化函数由前端预先写好,配置里只能引用它们的名字。确实要执行模型写的代码,就放到沙箱里执行。

易错点

  • 认为模型输出是"自己系统生成的",可以信任
  • 只在保存时清洗,流式渲染过程中却把未清洗的内容直接塞进 DOM
  • 自己写正则只过滤 <script> 标签:onerror 这类事件属性、javascript: 链接同样能执行脚本,清洗要交给成熟的库

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。