多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

DOM型XSS攻击原理与防御:从危险源到漏洞汇的完整攻防指南

DOM型XSS攻击原理与防御:从危险源到漏洞汇的完整攻防指南 1. 从一次真实的“点击劫持”事件说起几年前我负责的一个内部管理后台收到了一份用户反馈说他在点击某个“导出报表”按钮后自己的账号在另一个标签页里自动关注了几个完全不相关的内部账号。起初我们以为是后端接口被撞库或者会话劫持排查了一圈认证和日志毫无头绪。直到我们开始仔细审视那个“导出”按钮的前端实现代码问题才浮出水面。那个按钮的点击事件处理函数里有一段为了“灵活”而写的代码它从当前页面的URL哈希location.hash中读取一个参数然后直接用eval()去执行一个拼接后的字符串目的是动态调用不同的数据格式化函数。攻击者只需要构造一个特定的链接诱使已登录的管理员点击就能利用这个eval执行任意脚本悄无声息地完成各种操作。这就是一个典型的DOM型XSSCross-Site Scripting漏洞它不经过服务器直接在受害者的浏览器里由JavaScript解析执行因此传统的WAFWeb应用防火墙和基于服务端输入输出的过滤机制完全失效。DOM XSS全称Document Object Model-based Cross-Site Scripting是前端安全领域一个既古老又常新的议题。说它古老是因为其原理与Web诞生之初就存在的JavaScript动态操作DOM的能力相伴相生说它常新是因为在现代前端框架如React、Vue、Angular和各种复杂单页应用SPA大行其道的今天产生DOM XSS的“危险源”和“接收器”Sink变得更加多样和隐蔽。它不像反射型或存储型XSS那样恶意载荷会明显地在HTTP请求和响应中“旅行”DOM XSS的攻击载荷可能完全隐藏在URL片段、本地存储LocalStorage甚至页面初始的静态脚本中只在客户端侧被触发。这使得它的检测和防御更具挑战性也要求前端开发者必须具备更深层次的安全意识。本文将深入拆解DOM XSS的完整攻击链路、核心原理、在现代开发中的常见“坑点”并提供一套可落地的防御与测试方案。2. DOM XSS攻击链路的深度拆解源、流与汇要理解DOM XSS必须彻底搞清楚它的三个核心环节源Source、流Flow和汇Sink。这是一个动态的数据污染过程。2.1 危险源数据从哪里来源指的是那些可以被用户控制或影响的输入点。在DOM XSS的语境下源通常是浏览器环境下的一些特定对象或属性。很多人只记得location.search或location.hash但实际上危险源远不止这些URL相关对象这是最经典的源。document.URL/window.location.href整个URL字符串。document.location/window.location的各个属性search查询参数、hash片段标识、pathname路径。document.referrer当前页面的来源页地址。攻击者可以控制一个恶意页面诱导用户点击链接跳转到目标页面从而控制referrer值。Web存储与通信localStorage/sessionStorage如果应用将用户输入未经充分处理就存入后续又从存储中读取并动态使用就可能成为源。document.cookie如果Cookie值被恶意设置或篡改。window.name这个属性可以在页面跳转时被保留常被用于跨域通信但也可能被滥用。postMessage消息用于跨窗口通信如果消息监听处理不当发送的消息内容就是源。用户直接输入input/textarea的value。window.prompt、confirm等对话框的返回值虽然不常见。其他浏览器对象document.baseURI。通过window.open或iframe操作其他窗口/框架的location。注意在现代框架中源可能被框架的响应式系统或路由库封装。例如在Vue Router中你通过this.$route.query获取参数在React Router中用useSearchParams()。这些API背后操作的依然是URL但它们提供了更抽象的接口有时会让开发者忘记其底层依然是用户可控的输入源。2.2 数据流污染是如何传递的数据从源被取出后需要经过JavaScript代码的处理和传递最终到达汇。这个传递过程就是“流”。流可以是简单的变量赋值也可以是复杂的函数调用链。分析数据流是代码审计和自动化工具如CodeQL检测DOM XSS的关键。例如// 源从URL获取参数 let userInput location.hash.substring(1); // 假设 #scriptalert(1)/script // 流经过一些处理这里可能没有或有一些字符串操作 let processedInput decodeURIComponent(userInput); // 解码 let finalContent 欢迎 processedInput; // 字符串拼接 // 最终finalContent 被传递给某个汇 document.getElementById(message).innerHTML finalContent; // 危险在这个简单的例子里数据流是清晰的location.hash-userInput-processedInput-finalContent-innerHTML。任何一环如果进行了不安全的处理如这里完全没有处理漏洞就产生了。2.3 危险汇漏洞在哪里爆发汇是指那些能够将字符串解析为HTML或JavaScript并执行的浏览器API或属性。当被污染的数据流到达汇XSS就被触发。第一类HTML注入汇。这些汇会将字符串当作HTML解析如果字符串中包含script标签或具有事件处理器如onerror,onclick的HTML元素就会执行脚本。element.innerHTMLelement.outerHTMLdocument.write()/document.writeln()element.insertAdjacentHTML()某些特殊属性如iframe.srcdoc第二类JavaScript执行汇。这些汇会直接执行JavaScript代码字符串。eval()万恶之源必须避免。setTimeout()/setInterval()当第一个参数是字符串时如setTimeout(alert(xss), 1000)。Function()构造函数new Function(alert(xss))()。location.href/location.assign()/location.replace()当赋值为javascript:...伪协议时如location.href javascript:alert(document.cookie)。第三类基于属性的汇。某些属性在赋值时如果值以javascript:开头也会执行。a标签的href属性a hrefjavascript:alert(1)点击/aiframe标签的src属性iframe srcjavascript:alert(1)form标签的action属性。第四类现代框架的潜在汇。这是更容易被忽视的。ReactdangerouslySetInnerHTML是显式的HTML注入汇。但即使不使用它如果通过不安全的操作将用户输入传递给了href或事件处理器也可能出问题例如a href{userControlledLink}如果userControlledLink是javascript:...。Vuev-html指令等同于innerHTML。同样不安全的属性绑定:hrefuserLink也存在风险。Angular[innerHTML]属性绑定。理解“源-流-汇”模型是分析和防御DOM XSS的基石。任何防御措施其核心思想都是对从源取出的数据进行严格的消毒或编码确保它即使到达汇也无法被解析为可执行的代码。3. 实战场景剖析那些意想不到的DOM XSS触发点光有理论不够我们结合一些真实和常见的场景看看DOM XSS是如何“悄无声息”地发生的。3.1 场景一基于URL片段Hash的XSS与前端路由的恩怨这是开头案例的详细版。单页应用SPA普遍使用前端路由URL的哈希#之后的部分或HTML5 History模式下的路径被用来决定渲染哪个组件。考虑以下Vue RouterVue 2的“反面教材”// 不安全的路由守卫或组件逻辑 beforeRouteEnter(to, from, next) { // 假设攻击者构造URL: http://example.com/app#img srcx onerrorstealCookie() let message to.hash.substring(1); // 直接取hash得到 img srcx onerrorstealCookie() // 为了“动态显示”直接注入到某个容器 document.getElementById(dynamic-content).innerHTML 通知${message}; next(); }或者在一些老旧或自定义路由逻辑中开发者可能会手动解析window.location.hash来更新页面内容。如果解析后的值未经处理就直接用于innerHTML或类似的汇漏洞就产生了。为什么容易忽略因为开发者通常认为哈希不会发送到服务器确实#及之后的内容在HTTP请求中不包含所以觉得它是“安全”的客户端数据。但恰恰相反正因为它完全由客户端控制才是DOM XSS的绝佳源头。3.2 场景二第三方库与插件中的“暗桩”很多项目会引入第三方JavaScript库或插件如图表库、富文本编辑器、代码高亮库等。这些库为了“灵活”常常提供回调函数或配置项允许开发者传入HTML字符串或函数字符串。图表库某些库的tooltip.formatter或label.formatter属性如果支持返回HTML字符串并且这个字符串的来源如数据项字段用户可控就可能产生XSS。富文本编辑器这是重灾区。编辑器本身需要渲染HTML但如果其输出的HTML在后续被再次用innerHTML渲染且没有进行适当的过滤例如只在前端做了过滤但存储的原始数据在后端被其他系统调用时未过滤就可能出现问题。更隐蔽的是一些编辑器插件可能会在内容中插入带有onload或>// 手写的不安全模板渲染 function renderUserProfile(data) { let html div classprofile h2${data.username}/h2 p个人简介${data.bio}/p p来自a href${data.website}个人网站/a/p /div ; container.innerHTML html; }这里有两个问题data.bio如果包含HTML会被直接解析。data.website如果是一个javascript:伪协议链接点击后就会执行脚本。即使你用了现代框架如果错误地使用了字符串插值也可能绕过框架的保护机制。例如在React中// 错误这相当于直接设置innerHTML div{span${userInput}/span}/div // React会将整个字符串作为文本节点不会解析HTML但这样写极易导致混淆和后续错误。 // 正确的做法是始终将动态内容放在JSX表达式中或使用适当的属性绑定。3.4 场景四postMessage与跨域通信的漏洞window.postMessage是实现跨域通信的安全方式但前提是接收方要对消息来源进行严格的验证。// 不安全的消息监听器 window.addEventListener(message, function(event) { // 没有验证 origin document.getElementById(display).innerHTML event.data; }); // 攻击者可以在任意域名下的页面中嵌入 var targetWindow window.open(https://victim.com); setTimeout(function() { targetWindow.postMessage(img srcx onerroralert(document.domain), *); // 目标origin为*匹配任何目标 }, 1000);如果接收方页面盲目信任event.data并将其注入DOM就造成了XSS。这里的关键漏洞是缺少对event.origin的验证。4. 系统化防御从编码、验证到框架安全实践防御DOM XSS需要一套组合拳从开发习惯、编码规范到工具辅助层层设防。4.1 第一原则严格区分“数据”与“代码”这是最根本的哲学。永远不要将用户输入的数据当作代码来执行或解析。对于需要动态生成HTML的情况坚持以下优先级首选文本节点。如果只是显示内容使用textContent或框架的文本插值如React的{}Vue的{{ }}。它们会对内容进行HTML实体编码将、、等字符转义为lt;、gt;、amp;使其失去标签意义。次选安全的DOM API。如果需要创建带结构的元素使用document.createElement、setAttribute、appendChild等API来一步步构建而不是拼接HTML字符串。不得已使用受信任的消毒库。如果必须处理富文本HTML如用户评论中的加粗、斜体必须使用专业的、持续维护的HTML消毒库如DOMPurify。千万不要自己写正则表达式去过滤HTML和JavaScript的解析极其复杂正则无法覆盖所有边界情况。4.2 针对不同“汇”的编码策略根据数据最终注入的上下文Context需要进行不同的编码。这是防御XSS不限于DOM型的核心技术。注入点上下文危险字符示例编码方式示例输入scriptalert(1)/scriptHTML元素内容(如div.innerHTML,span.textContent的替代) HTML实体编码lt;scriptgt;alert(1)lt;/scriptgt;HTML属性值(如idvalue,classvalue) (取决于外层引号)HTML属性编码外层双引号时将转义为quot;JavaScript字符串(在script标签内或事件属性中) \ 换行符 UnicodeJavaScript Unicode转义\u003cscript\u003ealert(1)\u003c/script\u003eURL参数(在href,src,action等属性中)空格 ? # %URL百分比编码%3Cscript%3Ealert(1)%3C/script%3ECSS上下文; : ( )等CSS转义较少见但需注意现代框架的自动编码React、Vue、Angular等主流框架在默认情况下会对绑定到模板中的数据执行HTML内容上下文的编码。这是它们提供的最大安全红利。例如在React中div{userInput}/div是安全的。但是这个自动保护仅限于“文本内容”上下文。当你使用dangerouslySetInnerHTML(React)、v-html(Vue) 或绑定到href、src等属性时你就跳出了这个保护罩必须自己负责安全。4.3 实施严格的内容安全策略内容安全策略是一种由浏览器强制执行的、声明式的安全层通过HTTP响应头Content-Security-Policy来实施。它能极大地缓解包括DOM XSS在内的多种前端攻击。一个针对现代SPA的严格CSP示例Content-Security-Policy: default-src self; script-src self unsafe-inline unsafe-eval; style-src self unsafe-inline; img-src self data: https://cdn.example.com; font-src self; connect-src self https://api.example.com; frame-ancestors none; base-uri self;default-src self默认所有资源只能从当前域名加载。script-src self脚本只能从同源加载。这禁止了内联脚本如scriptalert(1)/script和onclick...从根本上杜绝了大量XSS。unsafe-inline和unsafe-eval是万不得已才加的加了它们CSP防XSS的效果就大打折扣。现代前端构建工具如Webpack可以将脚本打包成外部文件应尽量避免内联。style-src self unsafe-inline样式允许同源和内联因为CSS内联很常见。img-src,font-src,connect-src按需配置可信的资源来源。frame-ancestors none禁止页面被嵌入到iframe中防止点击劫持。base-uri self限制base标签的URL防止其被用来劫持相对路径的资源加载。部署CSP的步骤审计代码移除所有内联事件处理器和script标签。将脚本和样式提取为外部文件。先使用Content-Security-Policy-Report-Only头在报告模式下运行观察控制台报错调整策略。确认无误后切换到强制模式Content-Security-Policy。4.4 框架安全开发最佳实践React绝对避免直接使用dangerouslySetInnerHTML。如果必须确保传入的内容已经过DOMPurify消毒。对于链接使用安全的库来验证和清理URL或者确保href属性以http://或https://开头避免javascript:。使用relnoopener noreferrer处理target_blank的链接防止标签页劫持一种与XSS相关的攻击。Vue慎用v-html和React的dangerouslySetInnerHTML同理。在服务端渲染SSR场景下确保对初始状态initial state进行序列化时的安全性避免注入恶意脚本。通用原则永远不要使用eval()、new Function()、setTimeout(string)、setInterval(string)。对来自postMessage、localStorage、URL参数的所有数据都视为不可信进行验证和编码。使用类型系统TypeScript可以帮助识别一些潜在的不安全赋值。5. 漏洞挖掘与测试如何像攻击者一样思考防御的前提是能发现漏洞。DOM XSS的测试需要结合手动和自动化的方式。5.1 手动测试与代码审计寻找“源”在浏览器开发者工具中全局搜索location、document.URL、document.referrer、localStorage.getItem、sessionStorage、window.name、postMessage等关键词。跟踪“流”找到源后手动跟踪这个变量在JavaScript代码中的传递路径。看它是否经过了任何编码或过滤函数最终传递到了哪里。确认“汇”查看数据最终是否传递到了innerHTML、document.write、eval等危险函数或者是否被设置为element.attribute如href、src。构造Payload根据“汇”的上下文构造测试Payload。HTML上下文尝试img srcx onerroralert(1)、svg onloadalert(1)。属性上下文如果注入点在属性值里尝试 onmouseoveralert(1)或javascript:alert(1)对于href等。JavaScript上下文尝试-alert(1)-或;alert(1);//看是否能跳出字符串边界。5.2 自动化扫描与工具手动测试效率低需要借助工具。浏览器插件DOM Invader内置在Burp Suite浏览器中目前最强大的DOM XSS测试工具之一。它能自动识别源和汇并帮助构造复杂的Payload如利用原型链污染的Payload。PPScan一款专注于DOM XSS的Chrome插件可以辅助检测。动态扫描器Burp Suite Professional / OWASP ZAP这些Web漏洞扫描器具备一定的DOM XSS检测能力但它们更擅长传统XSS。对于复杂的、依赖特定用户交互的DOM XSS效果有限。定制化爬虫扫描对于大型应用可能需要结合Puppeteer或Playwright等浏览器自动化工具模拟用户操作覆盖更多动态生成的状态和路径再结合扫描引擎进行分析。静态代码分析SAST使用CodeQL、Semgrep等工具编写或使用现成的规则在代码层面检测“源到汇”的数据流。这对于在开发阶段发现潜在漏洞非常有效。5.3 利用漏洞测试平台练习对于想深入学习的人来说在可控环境中实战是必不可少的。PortSwigger Web Security Academy提供大量高质量的DOM XSS实验靶场从基础到高级涉及原型污染等并有详细的教程。Pikachu、DVWA、bWAPP这些综合漏洞练习平台也包含DOM XSS的关卡。CTF比赛像CTFshow的Web入门题目中常有考察DOM XSS的题目它们往往涉及更巧妙的绕过技巧。测试DOM XSS的关键是理解应用的客户端逻辑。有时漏洞触发需要一系列特定的用户操作如先点击A再输入B然后触发C。自动化工具可能无法覆盖这些路径因此手动测试和代码审计始终是不可替代的。6. 高级话题原型链污染与DOM Clobbering除了直接的源-汇流还有两种更高级、更隐蔽的客户端漏洞可能最终导致DOM XSS。6.1 原型链污染JavaScript中对象通过原型链继承属性和方法。攻击者如果可以控制对象属性的赋值并且赋值逻辑不当就可能污染Object.prototype等基础原型。之后应用中其他使用到该原型属性的地方行为就可能被篡改。// 一个易受攻击的合并函数 function merge(target, source) { for (let key in source) { if (source.hasOwnProperty(key)) { target[key] source[key]; // 如果key是 __proto__且环境允许就可能污染原型 } } return target; } let userInput JSON.parse({__proto__: {isAdmin: true}}); // 攻击者输入 let config {user: guest}; merge(config, userInput); // 之后应用中任何新创建的对象默认都有了 isAdmin: true 属性 let newObj {}; console.log(newObj.isAdmin); // true原型链被污染如果被污染的属性名恰好是应用后续用于生成HTML或决定脚本逻辑的关键字就可能导向DOM XSS。例如污染了Object.prototype.innerHTML那么之后任何未显式设置innerHTML的DOM元素其innerHTML属性都可能被攻击者控制。防御方法使用Object.assign()它不会遍历原型链进行对象合并或者使用更安全的库如 lodash 的_.merge在较新版本已修复。在递归合并时必须检查属性键名是否为__proto__、constructor、prototype等敏感关键字。6.2 DOM ClobberingDOM Clobbering 是一种利用HTML元素来覆盖JavaScript全局变量或对象属性的技术。当页面中存在name或id属性与全局变量同名的HTML元素时该元素会“覆盖”掉原来的变量。!-- 攻击者可以注入的HTML -- form idconfig input nameaction valueevilScript /form img nameouterHTML / script // 假设应用中有这样的不安全的代码 let config window.config; // 本意可能是获取一个JS对象但现在被DOM元素覆盖了 if (config config.action) { someElement.innerHTML config.action; // 现在 config.action 是字符串 evilScript } // 另一个例子覆盖方法 let img window.outerHTML; // 现在是一个DOM元素不是函数 // 后续调用 img() 会导致错误但可能改变程序流程 /script防御DOM Clobbering相对简单在引用可能被覆盖的全局变量时避免直接从window对象上隐式引用或者在使用前检查其类型。更根本的是避免将用户控制的id或name属性值直接与重要的全局变量名关联。7. 构建前端安全开发闭环DOM XSS的防御不是某个环节的工作而应该融入整个开发生命周期。设计阶段在技术方案评审时就考虑数据流的安全性。明确哪些数据来自用户它们会在客户端如何流动最终在哪里渲染。编码阶段启用安全编码规范在ESLint等代码检查工具中集成安全规则例如eslint-plugin-security可以禁止使用eval、innerHTML等危险模式。使用类型安全采用TypeScript为数据定义清晰的接口可以在编译期发现一些类型不匹配导致的安全问题。代码审查在Code Review中将安全作为必审项。重点关注所有处理用户输入、操作DOM、执行动态代码的地方。测试阶段SAST集成将CodeQL等静态分析工具集成到CI/CD流水线中每次提交都自动扫描。自动化DAST定期对测试环境或预发布环境进行动态安全扫描。人工渗透测试定期邀请安全团队或外部白帽子进行深度测试。部署与运维阶段强制CSP在生产环境部署严格的内容安全策略。安全监控与响应建立前端错误监控如Sentry配置规则捕获可能由XSS攻击引发的异常行为。同时制定安全事件应急响应预案。DOM XSS像是前端世界的“隐形杀手”它不按常理出牌躲在客户端的阴影里。对抗它没有一劳永逸的银弹需要的是开发者对“源-流-汇”模型的深刻理解、对安全编码原则的恪守、以及将安全思维贯穿于开发流程每一个环节的决心。从今天起审视你的代码看看是否有哪个innerHTML在等待一个邪恶的location.hash防微杜渐方能让应用在复杂的网络环境中屹立不倒。
返回列表