DOM型XSS深度解析:从原理到实战的客户端安全攻防 📅 2026/6/25 14:23:27 1. 项目概述从“反射”到“DOM”的认知跃迁很多刚接触Web安全的朋友一提到XSS跨站脚本攻击脑子里蹦出来的第一个场景可能就是在搜索框里输入一段scriptalert(1)/script然后点击搜索页面弹窗了。这确实是XSS但这是最经典、也最容易被WAFWeb应用防火墙拦截的反射型XSS。它的攻击载荷Payload是作为HTTP请求的一部分比如查询参数发送到服务器服务器“反射”回响应中最终在浏览器端执行。但今天我们要聊的是另一种更“狡猾”、更依赖前端逻辑、常常能绕过传统防护手段的XSS——DOM型XSS。它不经过服务器。是的你没看错攻击者的恶意脚本可能压根就没离开过你的浏览器。它的整个“作案过程”都发生在客户端的文档对象模型DOM解析与渲染环节。理解这一点是你从“脚本小子”迈向真正渗透测试员的关键一步。简单来说DOM型XSS的漏洞根源在于前端JavaScript代码不当地信任并操作了来自用户可控源的数据。这些数据源可能是document.location.hash、document.URL、document.referrer甚至是window.name。当这些数据被innerHTML、document.write、eval等“危险”的DOM API或全局函数处理时如果缺乏足够的净化攻击者精心构造的脚本就会被注入并执行。这个项目就是一次针对DOM型XSS的深度渗透实战。我们将模拟一个真实的前端应用场景从漏洞原理分析、攻击向量挖掘、Payload构造到绕过技巧和防御思路进行全链条的拆解。无论你是前端开发者想加固自己的代码还是安全爱好者想提升实战能力这篇文章都将提供一套清晰的“作战地图”。我会分享很多在真实渗透测试和代码审计中积累的“骚操作”和踩坑经验这些在标准文档里可找不到。2. DOM型XSS的核心原理与攻击面剖析2.1 为什么说DOM型XSS更“高级”要理解它的高级之处我们得先看看它与反射型、存储型XSS的本质区别。传统XSS反射/存储的恶意代码最终是作为服务器HTTP响应体的一部分HTML内容发送给浏览器的。因此安全人员可以在服务器层如WAF、输入过滤或网络层进行检测和拦截。而DOM型XSS的流程是浏览器收到一个“干净”的HTML文档 - 解析DOM树 -JavaScript代码执行- JavaScript代码从某个URL参数或客户端存储中读取了数据 - 用不安全的方式将这些数据写入了DOM - 浏览器渲染这部分新DOM触发了其中包含的脚本。整个恶意脚本的“诞生”和“执行”完全在客户端JavaScript引擎的控制下。服务器自始至终看到的可能只是一个正常的请求比如https://victim.com/page#userInput因为#后面的hash片段默认不会发送到服务器。这就让基于流量检测的防护手段几乎失效。2.2 关键的攻击源Source与危险的接收点Sink这是分析DOM型XSS的黄金法则。你必须像侦探一样追踪数据从“进入”到“造成危害”的完整路径。常见的数据源Sourcedocument.URL/window.location.href/window.location.search/window.location.hashdocument.referrer来源页面URLwindow.name跨页面传递的数据document.cookielocalStorage/sessionStoragepostMessage事件的数据通过URL.createObjectURL()创建的Blob URL是的这个也能玩出花危险的接收点Sink任何能将字符串当作HTML或JavaScript代码来解析、执行的API。HTML注入类element.innerHTML,element.outerHTML,document.write(),document.writeln()脚本执行类eval(),setTimeout()/setInterval()的第一个参数是字符串时Function()构造函数跳转类location.href,location.assign(),location.replace()当赋值为javascript:伪协议时事件处理器类element.addEventListener()在某些特定场景下但更常见的是内联事件处理器如onclick,onload,onerror的属性值被动态设置。现代前端框架的“危险”操作比如Vue.js的v-html指令、React的dangerouslySetInnerHTML属性它们本身就是为直接插入HTML设计的如果其内容源不可信就是高危漏洞。注意这里有一个非常重要的思维转变。在DOM型XSS中我们关注的不是“用户输入是否经过了服务器端过滤”而是“前端JS代码是否对来自Source的数据进行了安全的处理再送到Sink”。很多开发者的误区是只在后端做一次过滤就高枕无忧却忽略了前端复杂的、动态的数据流。2.3 一个经典的漏洞代码模式让我们看一段极度简化但非常典型的漏洞代码!DOCTYPE html html body script // 从URL的hash中获取数据不会发送到服务器 var userData decodeURIComponent(window.location.hash.substring(1)); // 危险操作直接将用户数据作为HTML写入DOM document.getElementById(output).innerHTML Hello, userData; /script div idoutput/div /body /html假设这个页面地址是http://example.com/page.html。一个正常的访问可能是http://example.com/page.html#Alice页面上会显示 “Hello, Alice”。但攻击者可以构造这样的链接http://example.com/page.html#img srcx onerroralert(document.cookie)。当受害者点击这个链接时userData变量的值就是img srcx onerroralert(document.cookie)。它被直接拼接进字符串并通过innerHTML插入到div#output中。浏览器解析这段新HTML时会创建一个img标签其src属性x加载失败随即触发onerror事件执行其中的JavaScript代码窃取用户的Cookie。关键点在于服务器收到的请求只是http://example.com/page.htmlhash部分#...在HTTP请求中是不可见的。因此任何服务器端的XSS过滤都对此无能为力。3. 实战环境搭建与手动漏洞挖掘3.1 选择合适的“靶场”纸上谈兵终觉浅。要真正理解DOM型XSS你必须亲手去挖。对于初学者我强烈推荐从以下开始DVWA (Damn Vulnerable Web Application)虽然它的XSS关卡主要演示反射型和存储型但其“DOM型XSS”关卡如果安装的版本包含是绝佳的起点。它通常提供一个有缺陷的下拉菜单或输入框让你直观感受基于document.write和location.hash的漏洞。PortSwigger Web Security Academy (Burp Suite官方靶场)这是目前最好的免费Web安全学习平台之一。它的DOM型XSS实验室由浅入深涵盖了从基础到各种绕过技巧的完整链条并且每个实验室都有详细的解决方案和解释。自己编写漏洞代码片段就像上面的那个简单例子。在本地创建一个HTML文件故意写一些有问题的JS代码。这是理解原理最快的方式你可以随意修改Source和Sink观察不同Payload的效果。我个人在带新人时通常会要求他们先自己写3-5个包含不同Source和Sink组合的漏洞页面然后再去挑战靶场。这样基础才牢固。3.2 手动挖掘流程与思维导图当你面对一个真实或仿真的Web应用时如何系统性地寻找DOM型XSS我的工作流如下第一步静态分析看代码搜索危险Sink在开发者工具的Sources面板或者如果有可能拿到前端JS代码全局搜索innerHTML、outerHTML、document.write、eval、setTimeout(、Function(等关键词。回溯数据流找到Sink后向上回溯看插入的数据变量从哪里来。是否来自location.search、location.hash、document.referrer等Source这个回溯过程可能跨越多个函数需要耐心。分析过滤逻辑在从Source到Sink的路径上数据经过了哪些处理是否有replace()、encodeURIComponent()、正则表达式过滤、黑名单等过滤是否彻底是否存在双写、大小写、编码绕过可能第二步动态分析浏览器中测试控制输入观察流向在URL参数、Hash、表单输入框等所有可能的输入点注入特殊的测试Payload如svg/onloadalert(1)或-alert(1)-。然后打开开发者工具的“Debugger”在疑似Sink的代码行设置断点。监控DOM变化使用开发者工具的“Elements”面板观察你的输入最终被插入到了DOM的哪个位置是否被HTML编码是否触发了事件属性。测试Source系统地测试每一个可能的Source。对于location.hash直接在地址栏修改。对于postMessage可以自己写一个恶意页面向目标页面发送消息。对于window.name可以构造一个中间页面设置window.name后再跳转到目标页。第三步构造突破性Payload闭合与逃逸如果数据被插入到现有的HTML标签属性如div id[user-input]或JavaScript字符串如var data [user-input];中你的Payload首先要做的就是闭合当前的上下文。在HTML属性中你需要先闭合引号然后引入事件处理器或新标签。如 onmouseoveralert(1)或scriptalert(1)/script。在JS字符串中你需要闭合字符串引号插入分号执行代码然后再注释掉剩余部分。如;alert(1);//。利用JavaScript协议对于location.href或a标签的href等Sink可以尝试javascript:alert(1)。但现代浏览器可能会对javascript:协议的内容进行一定编码需要测试。利用HTML5新特性与解析差异这是高级绕过技巧的宝库。例如SVG标签SVG元素内的某些事件处理器可能被更宽松地执行。svg onloadalert(1)。细节标签details ontogglealert(1)配合open属性或通过其他方式自动触发toggle事件。自定义数据属性有时过滤脚本会检查onxxx事件但忽略其他方式。可以结合DOM Clobbering等技巧。3.3 实操案例挖掘一个基于location.search的漏洞假设我们发现一段这样的代码function getSearchParam(key) { const urlParams new URLSearchParams(window.location.search); return urlParams.get(key); } const username getSearchParam(u); document.getElementById(welcome-msg).innerHTML Welcome back, strong${username}/strong!;看起来用了URLSearchParams这个现代API似乎很安全它确实能方便地获取查询参数。但问题出在最后一行它用模板字符串将username直接拼接进了innerHTML。攻击过程构造Payload我们需要让username的值突破模板字符串的上下文成为有效的HTML标签或事件。一个简单的测试http://victim.com/page?uimg srcx onerroralert(1)。结果分析URLSearchParams.get(u)会返回%3Cimg%20srcx%20onerroralert(1)%3E吗不会浏览器会自动解码URL编码所以它拿到的是img srcx onerroralert(1)。这个字符串被直接放入模板字符串最终innerHTML接收到的字符串是Welcome back, strongimg srcx onerroralert(1)/strong!。漏洞触发浏览器解析这段HTML创建了一个img元素其src指向一个不存在的x触发onerror事件执行alert(1)。这个案例的教训即使使用了现代的、安全的API来获取用户输入URLSearchParams只要最终使用了不安全的输出方式innerHTML漏洞依然存在。安全是一个链条最薄弱的一环决定了整体强度。4. 高级绕过技巧与混淆艺术当目标网站存在一些基础的过滤或WAF时直接使用简单的script标签或onerror事件可能会被拦截。这时就需要一些“骚操作”。4.1 编码与多重编码浏览器在解析的不同阶段会对编码进行解码。我们可以利用这种特性。HTML实体编码lt;代表gt;代表amp;代表。如果过滤函数只解码一次而输出点解码了两次就可能绕过。例如Payloadlt;img srcx onerroralert(1)gt;经过一次解码变成img srcx onerroralert(1)然后被innerHTML解析执行。JavaScript Unicode转义在JS上下文中\u003c是的Unicode转义。如果输入被放入eval()或setTimeout的字符串参数中且过滤不严可能有效。如eval(\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0029)会被执行为alert(1)。URL编码在location.hash或search中%3C是。如果前端JS代码使用了decodeURIComponent来解码那么传入%3Cimg%20srcx%20onerroralert(1)%3E就能成功。4.2 利用非标准的Sink和事件当常见的Sink被监控时可以寻找一些“偏门”的利用点。iframe的srcdoc属性srcdoc属性可以直接内嵌HTML内容。如果我们可以控制srcdoc的值就可以构造一个完整的恶意页面。例如iframe srcdocscriptalert(1)/script/iframe。object的data属性配合type某些老式技巧但现在仍值得一试。自动触发的事件除了onload,onerror还有onmouseenter,onfocus等但需要用户交互。我们可以寻找能自动触发的事件如details open ontogglealert(1)open属性会使它自动展开并触发ontoggle。或者利用input autofocus onfocusalert(1)autofocus属性会让元素自动获得焦点。4.3 DOM ClobberingDOM污染这是一种相对高级的技巧通过向DOM中插入一些具有特殊名称的HTML元素通常是带有id或name属性的表单元素来影响全局的JavaScript命名空间从而改变代码逻辑最终导向XSS。简单原理在浏览器中如果一个HTML元素如a idx具有id或name属性那么该属性值会作为一个全局变量window.x或document.x的引用。更神奇的是某些元素的属性也会被提升例如a idconfig hrefjavascript:alert(1)可能会产生window.config.href这样的属性。一个简化案例假设存在这样的漏洞代码if (window.config window.config.url) { window.location.href window.config.url; // Sink! }攻击者可以在页面中注入这样的HTMLa idconfig nameurl hrefjavascript:alert(document.domain)/a注入后window.config变成了对这个a元素的引用而window.config.url实际上访问的是该元素的name属性被提升为了url属性这里需要精确构造实际中更复杂可能利用HTMLCollection。通过精心构造可以让window.config.url的值变成javascript:alert(...)从而在location.href赋值时触发。实操心得DOM Clobbering 的利用条件比较苛刻需要代码中存在“先检查某个全局对象/属性是否存在然后再使用它”的逻辑模式。在实战中不常见但一旦发现往往能绕过非常严格的输入过滤因为攻击载荷看起来只是一些普通的HTML标签和属性不含任何JavaScript关键字或事件。5. 自动化工具辅助与漏洞验证手动挖掘虽然彻底但效率较低。在实际渗透测试中我们通常会结合自动化工具进行初筛。5.1 使用Burp Suite进行扫描和测试Burp Suite的Scanner和DOM Invader插件是神器。主动扫描配置好爬虫和扫描范围后Burp的主动扫描引擎能够检测到一些常见的DOM型XSS漏洞。但它并非万能对于需要复杂交互或条件触发的漏洞容易漏报。DOM Invader (Burp Pro版)这是专门为挖掘DOM型漏洞设计的浏览器内工具。它通过修改浏览器环境在可控的Source如URL参数、消息中注入独特的“Canary”令牌然后自动监控整个页面生命周期看这个令牌是否流向了危险的Sink如innerHTML,eval并高亮显示数据流路径。这极大地简化了回溯分析的过程。我的工作流是先用Burp的爬虫把目标网站结构摸清楚然后开启DOM Invader进行手动浏览。在浏览过程中DOM Invader会自动标记出存在潜在数据流从Source到Sink的页面。我再针对这些标记点进行深入的手动分析和Payload构造。5.2 使用浏览器开发者工具进行深度调试无论工具多强大最终验证和利用都离不开开发者工具。Console面板直接执行JavaScript来测试某些想法比如查看document.location.hash的当前值或者手动调用某个可疑函数。Debugger面板这是核心。在疑似包含Sink的JS文件行号上设置断点然后触发页面操作如提交表单、改变hash。当代码执行到断点时你可以查看所有变量的当前值单步执行F10步入函数F11观察数据是如何一步步流向Sink的。Network面板观察是否有额外的AJAX请求获取了数据这些数据也可能成为新的Source。特别是关注响应头中的Content-Type确保返回的是正确的application/json而不是text/html后者在JS解析时可能导致问题。5.3 漏洞验证与概念证明PoC构造找到可疑点后需要构造一个能稳定触发的PoC。最小化Payload从最简单的alert(document.domain)开始。alert(1)虽然通用但document.domain更能证明你确实在目标域下执行了脚本。考虑触发场景你的Payload是需要用户点击如onclick还是自动触发如onload,onerror如果是后者利用成功率更高。制作攻击URL将完整的Payload进行URL编码拼接到目标URL上。确保它能在无痕窗口或不同浏览器中稳定复现。编写漏洞报告一份好的报告应包括漏洞URL、触发步骤Step-by-Step、请求/响应截图、漏洞原理简要说明、修复建议。修复建议可以指向OWASP的DOM型XSS防护指南“在正确的上下文中对不可信数据进行编码”。对于HTML上下文使用textContent代替innerHTML或使用安全的API如DOMPurify库进行净化对于JavaScript上下文永远不要将不可信数据拼接进eval或Function应使用JSON解析。6. 防御策略与安全开发建议作为渗透测试的最后一环也是最有价值的一环我们需要知道如何修复它。防御DOM型XSS的核心思想是严格区分代码和数据。6.1 前端层面的根本性防御避免使用危险的Sink这是最有效的方法。问问自己真的需要用innerHTML吗绝大多数时候textContent或innerText足以安全地显示文本内容。真的需要用eval()吗99.9%的场景下都有更安全的替代方案。使用安全的API进行净化对于HTML插入使用经过严格安全审计的库如DOMPurify。它会在将字符串插入DOM前移除所有危险的标签和属性。cleanHTML DOMPurify.sanitize(userControlledInput);对于URL处理使用URL或URLSearchParams对象来解析和构造URL而不是手动拼接字符串。对于跳转确保目标URL是白名单内的或者至少检查协议不是javascript:。对于动态脚本避免使用eval和new Function。如果需要加载动态内容考虑使用JSON.parse()仅针对JSON数据或创建script标签并设置其src到可信来源。实施严格的上下文相关编码在HTML上下文中比如你要把数据放到标签之间div这里/div需要对,,,,等进行HTML实体编码。在HTML属性上下文中div attr这里除了上述字符空格和引号也需要特别注意。在JavaScript上下文中scriptvar x 这里;/script你需要进行JavaScript Unicode转义并处理好引号。在URL上下文中a href这里使用encodeURIComponent进行编码。好消息是很多现代前端框架如React, Vue, Angular的默认模板引擎已经帮你做了大部分上下文编码。但当你使用dangerouslySetInnerHTML或v-html时你就自己接管了安全责任。6.2 安全开发生命周期SDL集成安全培训让前端开发人员了解什么是DOM型XSS危险的Source和Sink有哪些。代码审计与组件分析将DOM型XSS的检测纳入代码审查清单。使用静态应用安全测试SAST工具扫描前端JavaScript代码寻找危险的数据流模式。实施内容安全策略CSPCSP是一个强大的深度防御措施。通过HTTP头Content-Security-Policy你可以告诉浏览器只允许执行来自特定来源的脚本禁止内联脚本包括onclick等事件处理器从而从根本上杜绝很大一部分XSS攻击。一个严格的CSP头像是给页面套上了一层坚固的盔甲。Content-Security-Policy: default-src self; script-src self https://trusted.cdn.com; object-src none;这个策略表示默认只加载同源资源脚本只能从同源或https://trusted.cdn.com加载完全禁止object等插件。6.3 我踩过的坑与心得不要依赖黑名单早期我曾尝试写一个函数过滤script、onerror等关键词。结果被大小写混淆ScRiPt、双写scrscriptipt、编码绕过#x73;#x63;#x72;#x69;#x70;#x74;教做人。安全领域白名单思维只允许已知安全的字符或模式永远比黑名单可靠。注意第三方库和依赖你写的代码可能很安全但你引入的某个古老的jQuery插件可能内部使用了$.html()来操作数据。定期用npm audit或类似工具检查依赖项的安全漏洞。DOM型XSS可能“沉睡”很久我曾遇到一个漏洞触发条件是用户从搜索引擎如Google的特定链接点击过来document.referrer包含特定参数并且页面上的一个广告区块加载失败触发onerror事件。这种组合条件在常规测试中极难发现。这提醒我们安全测试需要覆盖各种边缘场景和用户行为路径。自动化工具只是辅助。Burp Scanner报告了一个潜在的DOM XSS但你可能需要手动调整Payload十几次才能成功触发。工具告诉你“这里可能有问题”而你的经验和手动分析告诉你“如何把可能变成肯定”。这份“手动验证”的能力正是资深安全工程师的价值所在。