给AI对话做Markdown渲染防XSS

1 阅读3分钟

直接说重点:AI返回的Markdown,你绝对不能 v-html 或者 dangerouslySetInnerHTML 直接怼上去。 哪怕内容是你自家模型生成的。

为什么模型输出也得防

很多人觉得"又不是用户输入,模型还能害我?"——会的。

我遇到过两类真实情况:

  1. 用户在prompt里塞了<img src=x onerror=alert(document.cookie)>,让模型"复述一下我刚说的"。模型很乖,原样吐回来。你不过滤,XSS当场触发。
  2. RAG场景下,知识库文档本身就含恶意片段,模型检索到了照抄。

模型不是过滤器,它只是个复读机(有时候)。渲染层才是你最后一道防线。

我的方案:marked + DOMPurify

import { marked } from 'marked';
import DOMPurify from 'dompurify';

export function renderMarkdown(md: string): string {
  // 1. Markdown -> HTML
  const rawHtml = marked.parse(md, { breaks: true, gfm: true }) as string;

  // 2. 净化
  const clean = DOMPurify.sanitize(rawHtml, {
    ALLOWED_TAGS: [
      'p', 'br', 'strong', 'em', 'code', 'pre',
      'ul', 'ol', 'li', 'blockquote', 'a',
      'h1', 'h2', 'h3', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
    ],
    ALLOWED_ATTR: ['href', 'class'],
  });

  return clean;
}

顺序很关键:先转HTML再净化,不是反过来。DOMPurify要在真正的HTML上工作。

链接是重灾区

<a href="javascript:alert(1)">这种点击型XSS,DOMPurify默认会拦javascript:协议。但我还是手动加了一层钩子,顺便给所有外链补上rel

DOMPurify.addHook('afterSanitizeAttributes', (node) => {
  if (node.tagName === 'A') {
    node.setAttribute('target', '_blank');
    node.setAttribute('rel', 'noopener noreferrer nofollow');
  }
});

noopener是防新标签页通过window.opener反向操控你原页面——这个坑很多人不知道,光加target=_blank等于开了个后门。

流式渲染的额外麻烦(这个最坑)

AI是一个token一个token吐的。意味着你某一帧可能拿到的是:

这是一个未闭合的 **加粗

marked解析这种半截语法会产生奇怪结果,比如把整段后面都吃成加粗。我一开始每来一个token就重新marked.parse整个字符串,CPU直接拉满,而且渲染抖动。

后两个优化救了我:

  • 节流:用requestAnimationFrame攒着,一帧最多渲一次,别每个token都重渲。
  • 代码块兜底:流式没收完的代码块(```还没配对),我先当纯文本展示,等检测到闭合再交给marked。
let pending = '';
let scheduled = false;

function onToken(t: string) {
  pending += t;
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(() => {
    container.innerHTML = renderMarkdown(pending);
    scheduled = false;
  });
}

一个我没解决得很漂亮的点

代码高亮。我用了highlight.js,但流式下代码块一直在变,高亮要反复跑,长代码块会卡。我最后偷懒:只在 [DONE] 之后做一次高亮,流式过程中代码块就素颜展示。体验上有点跳,但性能稳,算个取舍。


最近我把这套渲染逻辑复用到一个新demo上——后端是我在一个拖拽配节点搭智能体的工具里搭的问答流,零代码配完直接给了我个接口。省下来的时间全花在打磨这个Markdown渲染器上了,值。

如果你后端不想自己折腾模型接入,讯飞的MaaS可以直接调现成模型,前端专心做安全渲染就行。你们防AImarkdown XSS还有什么野路子?评论区交个底。