在传统 Web 应用中,跨站脚本攻击(XSS)的防范体系已经相对成熟,比如现代框架(Vue、React)默认对模板插值执行转义,后端对用户提交的富文本进行严格过滤。然而,当系统引入生成式 UI(Generative UI)——即页面的组件树结构、动态 HTML 富文本、甚至事件回调均由大语言模型(LLM)根据 Prompt 实时生成并在前端动态挂载时,原有的安全防线被瞬间拉开了一道巨大的豁口。
很多团队天真地认为“大模型是受控的服务端输出,天然可信”。这种想法在黑客的 Prompt 注入(Prompt Injection)和越狱攻击面前脆弱得像一张薄纸。攻击者只需要在用户的评论区或外部输入中埋伏一句恶意的提示词,大模型在生成动态 UI JSON 时就可能夹带一段恶意脚本:例如在<img src="x" onerror="stealCookies()">,或者利用构造恶意的 SVG、甚至是伪装成合法组件的动态点击事件javascript:void(0)。
一旦前端未经防御就将大模型吐出的组件挂载到 DOM 树上,攻击者就能轻而易举地窃取用户的 Session Token、实施 CSRF 交易,甚至完全劫持整个控制台。
今天作为节后正式开工的第一天,我们来啃最硬的骨头:如何在生成式 UI 的渲染管线中,构建一套基于DOMPurify、安全白名单和属性消毒的物理隔离防线。
生成式 UI 面临的三大新型 XSS 威胁
大模型输出的动态 UI 不仅仅是字符串,它包含多层级的数据结构。常见的攻击面主要集中在以下三个环节:
[ 恶意外部 Prompt 注入 ] │ ▼ [ 大模型生成受污染的 UI JSON ] │ ├─► 1. 富文本属性注入: <p>你好 <script>alert(1)</script></p> ├─► 2. 伪造伪协议事件: <a href="javascript:eval(...)">查看详情</a> └─► 3. 动态组件参数穿透: <DynamicButton action="evilPayload">- 内联 HTML 片段恶意穿透:大模型为了排版美观,常常在组件的文本槽位中混入 HTML 标签。若前端直接使用
v-html渲染,恶意脚本将直接在宿主环境执行; - URI 伪协议(Scheme Hijack):在卡片组件的链接(
href)或图片(src)属性中,注入data:text/html;base64,...或javascript:代码; - 样式注入与 CSS 表达式攻击:通过恶意的
style属性注入@import或外部字体链接,发起恶意的 CSS 盲注(CSS Exfiltration),窃取用户输入框中的敏感信息。
核心防御战术:DOMPurify 深度定制与 Hook 消毒
在前端生态中,DOMPurify是最无可挑剔的 HTML 消毒利器。但在生成式 UI 场景下,直接使用默认配置是不够的,我们必须结合业务白名单实施深度定制。
1. 构建严格的生成式 UI 消毒策略配置
我们定义只允许在 AI 卡片中出现的安全标签和属性白名单,禁止任何危险标签(如<script>,<iframe>,<object>,<embed>,<form>):
// src/security/genUiSanitizer.ts import DOMPurify from 'dompurify'; /** * 生成式 UI 专属消毒配置 */ const SANITIZER_CONFIG: DOMPurify.Config = { // 仅允许安全的文本排版标签 ALLOWED_TAGS: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'b', 'strong', 'i', 'em', 'mark', 'small', 'del', 'ul', 'ol', 'li', 'blockquote', 'code', 'pre', 'hr', 'br', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'a', 'img' ], // 严格限定允许的属性白名单 ALLOWED_ATTR: [ 'href', 'src', 'alt', 'title', 'target', 'rel', 'class', 'style', 'width', 'height' ], // 强制为所有 <a> 标签注入安全属性,防止反向 window.opener 劫持 ADD_ATTR: ['rel', 'target'], // 严禁加载任何外部未受信任的 URI 协议 ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, // 彻底剔除 DOM Clobbering 攻击隐患 SANITIZE_DOM: true, // 保留安全文本内容,只剥离恶意标签 KEEP_CONTENT: true }; // 注册 DOMPurify 钩子,强制收紧超链接与图片行为 DOMPurify.addHook('afterSanitizeAttributes', (node) => { // 针对所有的链接,强制锁定外部打开与无 opener if (node.tagName === 'A') { node.setAttribute('target', '_blank'); node.setAttribute('rel', 'noopener noreferrer nofollow'); } // 针对图片,强制禁止未经认证的跨域内联协议 if (node.tagName === 'IMG') { const src = node.getAttribute('src') || ''; if (src.startsWith('data:') || src.startsWith('javascript:')) { node.removeAttribute('src'); } } // 针对行内 style,清除恶意的 url() 外部资源引用 if (node.hasAttribute('style')) { const style = node.getAttribute('style') || ''; if (/expression|url\(|javascript/i.test(style)) { node.removeAttribute('style'); } } }); export function sanitizeHtml(rawHtml: string): string { if (!rawHtml) return ''; return DOMPurify.sanitize(rawHtml, SANITIZER_CONFIG) as string; }2. 动态组件 Props 的递归消毒中间件
生成式 UI 的数据通常是一个深层 JSON 对象。恶意代码往往不仅藏在顶层 HTML 中,还可能藏在组件 Props 的嵌套对象里(例如表单项的默认值、卡片按钮的动作参数)。
在将 JSON 传递给动态组件渲染器(Dynamic Component Renderer)之前,必须通过递归中间件实施“深层消毒”:
// src/security/propsSanitizer.ts import { sanitizeHtml } from './genUiSanitizer'; /** * 递归清洗动态组件树的所有字符串属性 */ export function sanitizeComponentProps<T>(props: T): T { if (props === null || props === undefined) return props; // 字符串类型执行 HTML 转义与消毒 if (typeof props === 'string') { return sanitizeHtml(props) as unknown as T; } // 数组递归处理 if (Array.isArray(props)) { return props.map((item) => sanitizeComponentProps(item)) as unknown as T; } // 对象递归处理 if (typeof props === 'object') { const sanitizedObj: Record<string, unknown> = {}; for (const [key, value] of Object.entries(props)) { // 拦截高危的内联事件 Props (如 onClick, onError, onMouseOver) if (/^on[A-Z]/.test(key)) { console.warn(`[Security Alert] 拦截到大模型非法的内联事件 Prop: ${key}`); continue; } sanitizedObj[key] = sanitizeComponentProps(value); } return sanitizedObj as T; } return props; }CSP 内容安全策略:浏览器级底层兜底
除了前端代码逻辑层的校验,必须在 HTTP 响应头配置严密的内容安全策略(Content Security Policy, CSP),作为即使前端被攻破也无法执行恶意代码的终极保险箱:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-rAnd0m123'; object-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.yourdomain.com; base-uri 'self';object-src 'none':彻底封死 Flash、Applet 等古老插件的执行;script-src 'self' 'nonce-...':坚决移除'unsafe-inline'和'unsafe-eval',即使攻击者在 DOM 中强行注入了<script>节点,由于缺少合法的 Nonce 随机凭据,浏览器内核将拒绝解析执行。
实战效果与防御自测
我们在 CI 流水线中编写了端到端安全靶场测试用例,将包含 50 种高危 XSS Payload(如 SVG 向量攻击、DOM Clobbering、CSS 表达式)伪装在大模型的流式响应中:
- 经过
sanitizeComponentProps与DOMPurify双层拦截后,恶意脚本拦截率达到 100%; - 正常的 Markdown 排版、粗体、列表、公式和表格均完美保留,消毒过程在客户端仅引入不足0.4ms的开销,对首屏渲染几乎无感。
结语
在生成式 AI 时代,大模型不是圣人,它随时可能成为恶意攻击者借刀杀人的跳板。“永远不要相信客户端输入”是后端安全的第一法则;而对于生成式 UI 架构师而言,核心铁律则是:“永远不要相信大模型的输出”。用严密的白名单限制、深入底层的属性递归消毒和坚不可摧的 CSP 筑牢护城河,我们才能让生成式 UI 在安全的轨道上肆意狂飙。