前端防 XSS 的核心结论:Vue 的模板插值默认转义已经挡掉大部分注入,真正的风险集中在三个逃生窗口——渲染富文本、绑定动态链接、拼接不可信内容进 DOM。我们在企业级 AI 网关项目的对话页和管理页里,用”默认转义 + 白名单净化 + 协议校验 + CSP 兜底”四层防御,把 AI 生成内容和用户输入都当作不可信数据处理。落地顺序是:能不用 v-html 就不用,非用不可先过净化器,动态链接先验协议,最后用内容安全策略兜住意外穿透。
一、先认清 Vue 里哪些位置会漏
1.1 默认防护到底覆盖了哪里
Vue 的模板插值语法会自动对内容做 HTML 转义,渲染出来的永远是纯文本,这是框架层面的第一道防线。动态属性绑定同样会被转义,但属性绑定不会拦截恶意协议,这是很多人忽略的分界点。
| 渲染方式 | 是否转义 | 主要风险 |
|---|---|---|
| 模板插值(双花括号) | 转义 | 基本无风险 |
| 文本节点与属性绑定 | 属性值转义 | 协议注入风险 |
| v-html 指令 | 不转义,原样渲染 | 高风险,必须净化 |
| 自定义指令内直接操作 innerHTML | 不转义 | 同 v-html |
1.2 三个最容易出问题的场景
AI 网关项目里风险最集中的是三处:对话正文中的 Markdown 渲染(AI 生成内容里可能夹带构造好的 HTML 标签)、模型配置页的富文本描述、以及用户提交的链接地址。这三处的共同点都是”不可信内容要进 HTML 上下文”。
二、四层防御的落地代码
2.1 第一层:默认转义不主动关掉
对话页的纯文本消息一律用插值渲染,保持框架默认转义。只有确认需要富文本展示时才走净化流程,不为了一点点排版便利放弃默认防线。
2.2 第二层:白名单净化器统一收敛
所有需要渲染 HTML 的地方,统一走一个 useSafeHtml 组合式函数,内部用 DOMPurify 按白名单过滤。白名单只放基础排版标签,属性只放 href 等必要项,其余一律剥离。
// composables/useSafeHtml.ts
import DOMPurify from 'dompurify'
const ALLOWED_TAGS = ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'pre', 'code']
const ALLOWED_ATTR = ['href', 'target', 'rel']
export function useSafeHtml() {
function sanitize(raw: string): string {
return DOMPurify.sanitize(raw, {
ALLOWED_TAGS,
ALLOWED_ATTR,
ALLOW_DATA_ATTR: false,
})
}
return { sanitize }
}
对话正文的 Markdown 渲染同样要先过这道净化。我们禁用了 Markdown 渲染器里的原生 HTML 透传选项,让所有输出先落在白名单里再进 DOM。
2.3 第三层:动态链接先验协议
属性绑定不拦截恶意协议,因此对用户可控的 URL 单独做一次协议校验。合法协议只放 http、https、mailto,其余直接降级为空链接。
// utils/safeUrl.ts
const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:']
export function safeUrl(raw: string): string {
try {
const parsed = new URL(raw)
return ALLOWED_PROTOCOLS.includes(parsed.protocol) ? raw : '#'
} catch {
return '#'
}
}
用在新开窗口的链接上时,还要补上 rel="noopener noreferrer",防止被引用页面通过 window 对象反向控制原页面。
2.4 第四层:CSP 兜底
净化器只能保证已知载荷不生效,CSP 负责挡住未知变体。网关前端构建后在服务端返回内容安全策略响应头,限制脚本来源,生产构建下 Vue 3 不使用 eval 和动态编译,可以放心收紧:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'
connect-src 需要按实际 API 域名放行,SSE 长连接同源即可,跨域时单独加白。
2.5 富文本编辑器的粘贴入口
模型配置页如果接富文本编辑器,粘贴动作是漏网高发区。编辑器把剪贴板里的带格式内容转成 HTML 再进 v-html 链路,粘贴进来的内容往往自带内联事件属性。我们统一在粘贴事件里先做一次文本清洗,剥掉事件属性后再交给净化器,双重过滤比单靠运行时净化更稳。
三、管理页面容易忽略的两个点
模型配置页的表单校验只拦了格式,没拦协议。名称、描述、系统提示词这类字段的输入长度要有上限,链接字段在保存前用同一个 safeUrl 校验,前端和网关各验一遍,防止绕过前端直接调接口。路由参数同理,页面挂载时对路由 query 和 param 做类型校验,不符合预期格式直接跳 404,避免把异常值带进页面状态。
四、上线前的自检清单
- 用 Grep 搜一遍代码库里的 v-html 指令,逐个确认是否经过净化器;
- 对所有用户可控的链接绑定点,注入测试载荷并确认被降级为空链接;
- 在对话输入框粘贴构造过的富文本内容,确认渲染结果为纯文本或被剥离;
- 用浏览器开发者工具检查响应头,确认 CSP 已生效且未放行外部脚本域。
常见问题(FAQ)
Q1:净化器会误伤正常排版吗?
白名单覆盖常见排版标签,超出白名单的样式建议改用 Markdown 而非原始 HTML。
Q2:CSP 会不会影响 SSE 长连接?
同源请求不受影响,跨域需在 connect-src 中显式放行网关域名。
Q3:AI 生成内容也需要净化吗?
需要。模型输出不可信,同样按用户输入标准走白名单净化。