大模型应用XSS漏洞实战:从攻击到防御的完整解决方案
1. 项目概述:当大模型遇上XSS,一场意料之中的攻防战
最近在安全圈里,一个话题的热度持续攀升:大模型应用中的XSS漏洞。这听起来可能有点“关公战秦琼”的错位感——一个是前沿的AI技术,一个是存在了二十多年的经典Web漏洞。但恰恰是这种结合,暴露了当前大模型应用开发中一个普遍被忽视的“阿喀琉斯之踵”。我最近就深度参与了一个企业内部大模型知识库系统的安全审计与加固项目,完整地走了一遍从漏洞发现、原理分析到代码修复的全过程。今天,我就把这个实战案例掰开揉碎了讲给你听,不仅会还原漏洞场景,更重要的是,我会分享一套可以直接“抄作业”的防护代码和配置策略。
简单来说,这个项目里的系统,前端用Vue/React,后端是Spring Boot,接入了某款主流的大模型API,实现了一个智能问答和文档分析平台。问题就出在,当用户提问或系统展示大模型生成的内容时,如果这些内容里包含了精心构造的恶意脚本,而前端又直接将其作为HTML渲染了,那么经典的XSS攻击就借尸还魂了。这绝不是危言耸听,很多团队在拥抱大模型时,注意力都放在了模型效果、响应速度和成本上,却忘了最基本的输入输出安全。攻击者完全可以利用这一点,窃取用户的会话Cookie、发起未授权的操作,甚至将整个应用变成攻击跳板。接下来,我们就从攻击者的视角出发,看看这个漏洞是怎么被发现的,然后再切换到防御者的角色,一步步把它堵上。
2. 漏洞发现:一次针对大模型输出的定向渗透测试
2.1 测试环境与目标分析
我们的测试目标是一个部署在测试环境的智能客服系统。其核心流程是:用户在前端输入问题,后端将问题连同一些上下文发送给大模型API,获取回答后,再返回给前端渲染展示。前端为了追求“富文本”体验,部分区域使用了v-html(Vue)或dangerouslySetInnerHTML(React)来直接渲染模型返回的答案,以便正确显示换行、加粗等简单格式。
测试的第一步是信息收集。我们确认了前端框架、查看了网络请求,发现模型回答是以JSON格式返回,其中一个字段answer_content包含了完整的HTML片段。这是一个非常明确的危险信号:后端没有对模型输出做任何净化和转义,直接信任并下发了。
2.2 构造与投递恶意载荷
经典的XSS测试载荷如<script>alert(1)</script>在这里可能不会直接生效,因为大模型本身具有“安全意识”,可能会拒绝生成或改写明显包含恶意脚本的文本。因此,我们需要更隐蔽的测试方法。
方法一:上下文污染与指令混淆我们尝试在用户提问中,以“举例说明”、“请用代码演示”为名,诱导模型生成包含脚本的示例。例如,提问:“请写一段HTML代码,展示如何用JavaScript弹出一个对话框。” 模型可能会生成类似下面的“教学代码”:
<p>示例代码如下:</p> <pre><code><script>alert('Hello');</script></code></pre>关键在于,如果后端处理不当,模型返回的整个文本块被当成了HTML,那么<pre><code>标签内的实体字符<和>可能在某个环节被错误地解码还原成了真正的<和>,从而导致脚本执行。我们通过拦截代理修改响应,手动将<script>替换为<script>,成功触发了弹窗。
方法二:利用Markdown或特定格式解析缺陷许多系统为了美观,会将模型返回的Markdown转换为HTML。我们测试了诸如以下载荷:
这是一段正常文本。 ) // 利用图片链接协议 [链接](javascript:alert('XSS')) // 利用链接href如果使用的Markdown解析器版本老旧或配置不当,没有对生成的href或src属性进行协议过滤(白名单校验),就可能产生javascript:伪协议注入。
方法三:事件处理器与SVG/MathML向量当直接<script>标签被过滤时,我们转向HTML事件属性或其它可执行代码的向量:
<img src="x" onerror="alert('XSS')"> <svg><script>alert('XSS')</script></svg>在测试中,我们通过模拟“用户上传的、被模型引用的文档内容中包含此类向量”的场景,发现当模型在回答中提及或“引用”这些内容时,系统同样会不加处理地渲染,导致漏洞触发。
实操心得:测试大模型的XSS,不能只盯着直接的脚本注入。要思考模型作为“中间处理器”和“内容生成器”的特性,测试点应放在模型输出内容的解析与渲染环节,以及系统对模型输出内容的信任边界上。使用Burp Suite或类似的拦截工具,在模型返回的JSON响应中直接修改
answer_content字段进行测试,效率最高。
2.3 漏洞确认与影响评估
通过上述测试,我们确认了至少两处存储型XSS漏洞(用户恶意提问经模型“转述”后存储,其他用户查看时触发)和一处反射型XSS漏洞(恶意回答即时返回并在当前页面渲染触发)。攻击者可以利用此漏洞:
- 盗取用户凭证:窃取登录用户的Session Cookie或Token。
- 冒充用户操作:在用户不知情下,以其身份发送消息、修改资料。
- 客户端钓鱼:在应用内伪造登录弹窗,诱骗用户输入密码。
- 传播恶意软件:通过插入恶意脚本,引导用户下载或执行恶意程序。
漏洞的根本原因在于:系统在设计上完全信任了大模型API的输出,将其视为“安全数据”,而忽略了模型本质上是一个不受控的、可能被恶意输入引导的“内容生成器”。
3. 漏洞原理深度拆解:为什么大模型应用是XSS的温床?
3.1 信任链的断裂:从用户输入到模型输出
在传统Web应用中,安全防御的核心是建立清晰的信任边界。通常,我们“不信任任何用户输入”,所以会对所有来自前端、接口的用户数据进行严格的验证、过滤和转义。这个链条是:不可信用户输入 -> 严格过滤 -> 安全的数据 -> 安全地渲染。
但在大模型应用中,这个链条被拉长并扭曲了:用户输入 -> 大模型(不可控的复杂处理单元)-> 模型输出(新的、潜在的不可信数据)-> 直接渲染。开发者常常错误地将“经过大模型处理”等同于“被净化了”,认为模型具有“理解”和“过滤”能力。然而,大模型本质上是基于概率生成文本,它没有安全语义理解能力。它可能被精心设计的提示词(Prompt)诱导,生成符合语法但包含恶意代码的内容;也可能在“学习”了训练数据中的不安全样本后,复现出危险代码。
3.2 渲染管道的安全缺失
漏洞爆发的最后一个环节是前端渲染。为了灵活展示模型生成的、带有简单格式的内容,开发者倾向于使用以下危险操作:
innerHTML/v-html/dangerouslySetInnerHTML:这些API会直接将字符串作为HTML解析并插入DOM,如果字符串中含有脚本,则必然执行。- 不安全的动态属性绑定:例如
:href="modelOutputLink",如果modelOutputLink是模型返回的javascript:...,则构成漏洞。 - 第三方库的滥用:如使用未正确配置的Markdown转HTML库(如
marked旧版本默认不转义HTML)、富文本编辑器库等,这些库可能默认不安全或需要显式开启安全模式。
3.3 与训练数据投毒的结合风险
这是一个更深层次的威胁。如果攻击者能够污染大模型的训练数据(例如,在开源代码库、论坛讨论中植入特定的恶意代码模式),那么模型在生成相关代码建议时,就可能直接输出带有后门或漏洞的代码。当这类输出被应用不加处理地展示和采用时,就构成了供应链攻击。虽然本次实战未涉及此层面,但它是大模型安全必须考虑的宏观风险。
4. 修复方案设计与核心代码实现
修复的核心思路是“永不信任,始终验证”,将大模型的输出重新纳入不可信数据范畴,并在输出到最终用户界面的每一个环节施加防护。我们采用了一种深度防御的策略。
4.1 后端修复:输出过滤与内容安全策略(CSP)
第一道防线:强制输出转义与过滤在后端(Spring Boot)控制器返回模型结果前,对answer_content等字段进行严格的HTML转义。但注意,不能简单地转义所有字符,否则会破坏原有的合法格式(如<br>)。我们需要一个更智能的“白名单”过滤。
我们引入了OWASP Java HTML Sanitizer这个强大的库,它允许我们定义一个允许的标签和属性白名单。
import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; @Service public class ContentSecurityService { // 1. 定义针对大模型输出的HTML净化策略 private static final PolicyFactory MODEL_OUTPUT_POLICY = new HtmlPolicyBuilder() .allowElements("p", "br", "strong", "em", "ul", "ol", "li", "code", "pre", "blockquote") .allowAttributes("class").onElements("code", "pre") .allowUrlProtocols("https", "http") // 只允许http/https链接 .allowAttributes("href").onElements("a").requireRelNofollowOnLinks() // 链接自动加nofollow .allowAttributes("src").onElements("img").matching(ContentSecurityService::isValidImageUrl) .toFactory(); private static boolean isValidImageUrl(String url, String elementName) { // 简单示例:确保是图片URL且协议合法 return url.startsWith("https://") || url.startsWith("http://"); } // 2. 净化方法 public String sanitizeModelOutput(String rawModelOutput) { if (rawModelOutput == null) { return ""; } // 使用策略进行过滤,不允许的标签和属性将被移除或转义 return MODEL_OUTPUT_POLICY.sanitize(rawModelOutput); } }在控制器中调用:
@PostMapping("/chat") public ApiResponse<ChatResult> chat(@RequestBody ChatRequest request) { // ... 调用大模型API获取原始回答 rawAnswer ... String safeAnswer = contentSecurityService.sanitizeModelOutput(rawAnswer); // ... 将safeAnswer返回给前端 ... }第二道防线:部署严格的内容安全策略(CSP)CSP是一个重要的浏览器安全特性,通过HTTP头告诉浏览器哪些资源可以加载和执行,是缓解XSS的终极武器之一。即使有恶意脚本被注入,严格的CSP也能阻止其执行。
我们在Spring Boot的配置或全局过滤器中添加CSP头:
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @Configuration public class SecurityConfig implements WebMvcConfigurer { @Bean public FilterRegistrationBean<Filter> cspFilter() { FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>(); registration.setFilter(new Filter() { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) res; // 一个非常严格的CSP策略示例,禁止任何内联脚本和eval String cspHeader = "Content-Security-Policy: " + "default-src 'self'; " + "script-src 'self' https://trusted.cdn.com; " + // 只允许来自自身和特定CDN的JS "style-src 'self' 'unsafe-inline'; " + // 允许内联样式,必要时可收紧 "img-src 'self' https: data:; " + // 允许图片来自自身、https协议和dataURL "connect-src 'self' https://api.bigmodel.com; " + // 限制可连接的API端点 "frame-ancestors 'none';"; // 禁止被嵌套 response.setHeader("Content-Security-Policy", cspHeader); chain.doFilter(req, res); } }); registration.addUrlPatterns("/*"); return registration; } }注意事项:CSP策略的制定需要谨慎,过于严格可能会破坏网站正常功能。建议先在“报告模式”(
Content-Security-Policy-Report-Only)下运行,观察控制台报告,逐步调整到合适的策略后再强制执行。
4.2 前端修复:安全的渲染实践
后端做了过滤和CSP,前端也不能掉以轻心,这是最后一道关卡。
原则:能不用v-html就不用对于纯文本展示,坚决使用文本插值{{ modelAnswer }}或React的{modelAnswer},Vue和React会自动进行HTML转义。
必须渲染富文本时,使用经过安全审计的库如果确实需要展示模型返回的简单格式(如加粗、列表),应使用专门的安全HTML渲染库。
Vue 3 示例(使用
v-html配合净化函数): 虽然Vue的v-html有风险,但如果我们已经在后端和前端双重净化了数据,可以谨慎使用。更好的做法是使用一个安全的渲染组件。<template> <div> <!-- 危险做法:直接渲染 --> <!-- <div v-html="modelAnswer"></div> --> <!-- 安全做法1:使用计算属性进行前端二次净化(可选,增加防御深度) --> <div v-html="sanitizedAnswer"></div> <!-- 安全做法2:使用专门的渲染组件,如 `vue-dompurify-html` --> <!-- <div v-dompurify-html="modelAnswer"></div> --> </div> </template> <script setup> import { computed } from 'vue'; import DOMPurify from 'dompurify'; // 前端净化库 const props = defineProps(['modelAnswer']); const sanitizedAnswer = computed(() => { // 配置DOMPurify,与后端白名单保持一致 const clean = DOMPurify.sanitize(props.modelAnswer, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote', 'a', 'img'], ALLOWED_ATTR: ['class', 'href', 'rel', 'src'], ALLOWED_URI_REGEXP: /^(https?:)?\/\/.+/i // 限制URL协议 }); return clean; }); </script>React 示例(使用
dangerouslySetInnerHTML配合净化):import React from 'react'; import DOMPurify from 'dompurify'; function SafeRenderComponent({ modelAnswer }) { const sanitizedHtml = DOMPurify.sanitize(modelAnswer, { // ... 同样的安全配置 ... }); return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />; }
安全地处理动态属性对于模型输出中可能包含的链接,永远不要直接绑定到href或src。
<template> <!-- 危险! --> <!-- <a :href="modelOutputLink">点击</a> --> <!-- 安全:使用方法进行校验 --> <a :href="safeLink(modelOutputLink)">点击</a> </template> <script> methods: { safeLink(rawLink) { if (!rawLink) return '#'; // 简单的协议校验 if (rawLink.startsWith('http://') || rawLink.startsWith('https://')) { return rawLink; } // 对于不符合条件的链接,返回一个安全的值或进行编码 return '#'; // 或者更严格:return `javascript:void(0);` 但注意这本身也有极小风险,通常用'#'即可 } } </script>4.3 大模型层提示词工程加固
除了在应用层防护,我们还可以尝试在调用大模型时,通过系统提示词(System Prompt)来约束其输出格式,降低生成恶意内容的风险。但这只能作为辅助手段,不能替代应用层的安全措施。
例如,在调用API时,可以附加这样的提示:
你是一个安全的AI助手。请遵守以下规则: 1. 你的所有回答都将以纯文本或安全的Markdown格式呈现。 2. 绝对不要在输出中包含任何HTML标签,如<script>, <img onerror>, <svg>等。 3. 如果用户要求你生成代码,请确保代码示例中的任何HTML或JavaScript片段都以代码块形式呈现,并确保其内容不会被解释为可执行代码。 4. 不要生成任何包含“javascript:”伪协议的链接。5. 测试验证与上线前检查清单
修复完成后,必须进行严格的回归测试和安全验证。
5.1 自动化安全测试
- DAST(动态应用安全测试):使用ZAP、Burp Suite Professional等工具,对聊天接口进行主动扫描,重点测试XSS漏洞。
- SAST(静态应用安全测试):使用SonarQube、Checkmarx等工具扫描代码,查找是否还存在不安全的
innerHTML使用或未经验证的输入流。 - 依赖项扫描:使用OWASP Dependency-Check或Snyk检查项目中使用的库(特别是Markdown解析、HTML净化库)是否存在已知漏洞。
5.2 手动渗透测试复现
重新执行漏洞发现阶段的所有测试用例:
- 尝试注入各种XSS载荷。
- 检查网络响应中CSP头是否正确设置。
- 验证前端渲染后的DOM中,恶意脚本是否已被转义或移除(查看元素源码,而不是检查页面效果)。
- 使用浏览器开发者工具的控制台,查看是否有CSP违规报告。
5.3 上线前安全检查清单
将以下清单整合到你的CI/CD流程或上线核对表中:
| 检查项 | 具体内容 | 验证方法 |
|---|---|---|
| 后端输出净化 | 所有从大模型API或其他外部数据源获取的内容,在返回前端前是否经过HTML净化(白名单过滤)? | 代码审查,查看对应Service层方法。 |
| CSP头配置 | 生产环境HTTP响应头是否包含有效的、非报告模式的Content-Security-Policy? | 使用浏览器开发者工具Network标签查看响应头,或使用curl -I命令。 |
| 前端安全渲染 | 是否完全避免了不必要的v-html/dangerouslySetInnerHTML?必须使用时,是否配合了前端净化库(如DOMPurify)? | 代码审查,全局搜索危险API。 |
| 动态属性安全 | 所有动态绑定的href、src等属性,其值是否经过协议验证或白名单过滤? | 检查相关工具函数或计算方法。 |
| 依赖库安全 | 使用的HTML/Markdown处理库是否为最新安全版本?其默认配置是否安全? | 运行npm audit或dependency-check。 |
| 错误处理 | 当净化过程出现异常时,是否有降级策略(如返回空字符串或转义后的纯文本)? | 查看代码的异常处理逻辑。 |
6. 总结与延伸思考
这次实战让我深刻体会到,在集成任何新技术时,尤其是像大模型这种能力强大但边界模糊的技术,安全必须作为第一性原理来考虑。大模型并没有改变Web安全的基本规则,反而因为其复杂性和不可预测性,引入了新的攻击面。修复一个漏洞并不难,难的是建立起一套持续的安全意识和防护体系。
我个人在实际操作中的体会是:不要试图依赖大模型来保证安全。它的核心任务是生成内容,而不是做安全审计。安全的责任必须牢牢掌握在应用开发者手中。将模型输出视为“有毒的、需要消毒的用户输入”,是构建安全大模型应用的正确心态。
最后再分享一个小技巧:在团队内部,可以建立一个“大模型安全测试用例库”,把这次发现的以及能想到的各种诱导生成恶意代码的Prompt收集起来,作为每次版本迭代的必测项。这能有效防止同类漏洞在未来的功能开发中复发。
安全是一个过程,而不是一个状态。随着大模型能力的演进,攻击者的手段也会翻新。保持警惕,持续学习,将安全设计融入每一个开发环节,是我们能为自己产品筑起的最坚固的防线。
