1. 项目概述:为什么DOM型XSS值得你投入精力?
如果你是一名Web安全爱好者、渗透测试工程师,或者是一名希望写出更安全代码的前端开发者,那么“DOM型XSS”这个名词你一定不陌生。但很多时候,我们可能只是听说过它“比较隐蔽”、“绕过WAF”,对其真正的挖掘利用和防御细节却一知半解。我见过太多安全报告里,测试人员把反射型XSS的Payload往URL参数里一塞,弹个窗就完事了,但对于那些不经过服务端、纯粹在浏览器里“兴风作浪”的DOM型XSS,往往束手无策,或者干脆选择忽略。
这正是我想写这篇长文的原因。DOM型XSS(Document Object Model Cross-Site Scripting)是一种特殊类型的跨站脚本攻击,它的恶意代码从不接触服务器。攻击者精心构造的恶意数据,通过URL的片段(hash)、前端逻辑处理等方式,直接在受害者的浏览器中被解析并执行。这意味着,传统的服务端输入过滤、WAF(Web应用防火墙)很可能完全失效。对于攻击方,这是穿透防线的利器;对于防御方,这是视野之外的盲区。
从零基础到精通,这条路我走过,踩过不少坑,也积累了许多在实战中真正好用的技巧。这篇文章的目的,就是把我这些年的经验浓缩成一套可操作的方法论。我不会只给你讲几个
innerHTML
或
eval
的案例就结束,那太浅了。我会带你深入DOM操作的核心,理解数据流如何在JavaScript中“流动”并最终变得危险,分享我手动审计和工具辅助挖掘的具体步骤,甚至聊聊那些在真实漏洞赏金(Bug Bounty)项目中,让我成功提交高危报告的“骚操作”。无论你是想系统学习前端安全,还是急需提升在渗透测试中的挖洞能力,收藏这一篇,反复实践,就够了。
2. DOM型XSS核心原理深度拆解
要精通挖掘,必须先吃透原理。很多人对DOM型XSS的理解停留在“用
location.hash
触发”,这远远不够。它的本质是:
不可信的数据源,流经了危险的JavaScript接收器(Sink),并且中间缺少了正确的编码或验证。
2.1 理解关键三要素:源、汇与数据流
这是分析任何DOM型XSS的思维框架。你可以把它想象成一条污染的河流:源头被投毒,河道没有净化处理,最终流进了居民的水库。
1. 源(Source):攻击者可控的输入点 这是恶意数据的入口。DOM型XSS的源非常多样,且常常被忽略:
-
location对象家族 :这是最经典的源。-
location.hash:URL中#后面的部分,如https://example.com/page#<script>alert(1)</script>。它 不会 发送到服务器。 -
location.search:URL中?后面的查询字符串,如?id=123。这部分通常会发送到服务器,但JavaScript可以从前端直接读取并处理,如果处理不当,同样构成DOM型XSS。 -
location.pathname:URL的路径部分。 -
document.URL/document.documentURI:完整的URL字符串。
-
-
document.referrer:浏览器提供的,表示当前页面来源页面的URL。攻击者可以构造一个恶意页面,链接到目标页面,从而控制referrer的值。 -
document.cookie:当前页面的Cookie。如果Cookie值能被攻击者部分控制(例如,通过子域名劫持、其他漏洞设置),也可能成为源。 -
Web存储
:
localStorage、sessionStorage。如果应用将用户输入未经处理就存入这里,并在另一个页面或同一页面后续操作中读取使用,就可能引入风险。 -
window.name:这个属性可以在页面跳转甚至跨域时保持不变,常被用于跨域通信,但也可能成为XSS的传递渠道。 - 客户端反序列化数据 :从服务器获取的JSON数据,如果其中包含了本应由服务器净化的用户可控内容,前端直接拼接使用,也属于DOM型漏洞的范畴。
实操心得 :在审计时,不要只盯着URL参数。用浏览器的开发者工具,在Console里输入
document.cookie、document.referrer、localStorage.getItem(‘key’)看看,说不定有惊喜。我曾在一个电商网站发现,它将用户搜索词用Base64编码后存到sessionStorage,然后在推荐模块直接innerHTML出来,成功构造了存储型DOM XSS。
2. 汇(Sink):危险的JavaScript函数或属性 这是执行恶意代码的地方。数据流到这里时,如果内容是HTML或JavaScript字符串,就会被浏览器解析执行。
-
HTML注入汇点
:
-
element.innerHTML -
element.outerHTML -
document.write() -
document.writeln()
-
-
JavaScript代码执行汇点
:
-
eval():万恶之源,直接执行字符串作为代码。 -
setTimeout()/setInterval():第一个参数可以是字符串,会被当作代码执行。例如setTimeout(“alert(‘xss’)”, 1000)。 -
Function()构造函数:new Function(‘alert(1)’)。 -
location相关:location.href、location.assign()、location.replace(),当赋值为javascript:伪协议时。
-
-
其他常见汇点
:
-
element.setAttribute(name, value):当name参数可控且为onerror、onclick等事件处理器时。 -
element.appendChild()/insertBefore()等:如果插入的节点内容可控。 -
jQuery的
$()/html()/append()等方法:如果未正确配置或使用旧版本。
-
3. 数据流(Data Flow):从源到汇的路径
这是最考验分析能力的一环。源代码往往不是
source -> sink
这么直接。它可能经过多个函数的传递、字符串的拼接、条件判断的分支。
// 一个简单的例子
function getSearchParam(key) {
const urlParams = new URLSearchParams(location.search);
return urlParams.get(key);
}
const userId = getSearchParam(‘id’); // 源:location.search
document.getElementById(‘user-info’).innerHTML = `用户ID: ${userId}`; // 汇:innerHTML
// 攻击:?id=<img src=x onerror=alert(1)>
在这个例子里,数据流是:
location.search
->
URLSearchParams.get()
->
userId
变量 -> 模板字符串 ->
innerHTML
。我们需要在脑海中或通过工具追踪这条路径。
2.2 与反射型、存储型XSS的本质区别
很多人会混淆,这里必须彻底厘清。假设攻击Payload是
<script>alert(‘xss’)</script>
。
-
反射型XSS
:Payload作为请求的一部分(如URL参数
?q=<script>...</script>)发送到 服务器 ,服务器在响应页面中 原样反射 回这个Payload,浏览器执行。- 关键 :Payload出现在服务器响应体(HTML)中。你在页面源代码(View Source)里能看到它。
-
存储型XSS
:Payload被发送到服务器后,
存储
在数据库、文件等地方(如评论内容)。当其他用户访问某个页面时,服务器从存储中取出Payload并放入响应中,浏览器执行。
- 关键 :Payload被持久化,受害者无需点击特定链接,访问正常页面即可中招。
-
DOM型XSS
:Payload作为客户端JavaScript的
输入数据
(如
location.hash)。前端的JavaScript代码(可能是服务器下发的静态脚本) 读取这个输入,并用自己的逻辑 将其写入DOM的某个危险位置(汇点),导致执行。- 关键 :Payload 可能从未出现在服务器响应或页面源代码中 。它只存在于客户端的JavaScript执行上下文里。你必须使用浏览器的“开发者工具” -> “元素”面板,查看 实时DOM树 ,或者使用“调试器”跟踪JavaScript执行,才能看到Payload是如何被动态添加进去的。
这个区别导致了根本性的影响:
-
绕过服务端防护
:WAF、输入过滤规则可能完全检测不到,因为恶意请求参数(如
#后的内容)根本不发往服务器。 - 检测难度大 :自动化扫描器通常通过分析HTTP请求和响应来发现漏洞。对于DOM型XSS,扫描器需要能够执行并分析JavaScript,模拟浏览器环境,技术门槛高很多。
- 修复责任转移 :修复漏洞的关键在于前端JavaScript代码,需要确保从“源”到“汇”的数据流经过了正确的编码或验证。
3. 手工挖掘与审计实战指南
知道了原理,我们开始动手挖。我强烈建议从手工审计开始,这能帮你建立对代码和数据流的“直觉”。
3.1 第一步:识别潜在的“源”和“汇”
打开目标网站,按下F12开启开发者工具。
1. 全局搜索“危险函数”(汇点):
在“源代码”(Sources)标签页,或者在整个工作区使用
Ctrl+Shift+F
(Windows) /
Cmd+Opt+F
(Mac) 进行全局搜索。关键词列表如下,建议从最危险的开始:
innerHTML
outerHTML
document.write
document.writeln
eval
setTimeout
setInterval
Function
location.href
location.assign
.insertAdjacentHTML
jQuery.html
jQuery.append
jQuery.before
jQuery.after
React dangerouslySetInnerHTML
Vue v-html
每找到一个,就点击过去,看看它操作的数据是什么。
2. 回溯数据来源(找源):
当你定位到一个
innerHTML = something
时,问自己:
something
这个值从哪里来?
- 它是一个变量吗?双击变量名,查找它的定义和赋值位置。
- 它是函数调用的结果吗?去查看那个函数的返回值。
-
它是否来自
location.search、location.hash、document.referrer或document.cookie? -
它是否来自
window.name、localStorage或sessionStorage? -
它是否来自
fetch()/XMLHttpRequest从服务器获取的数据?如果是,尝试篡改API响应(通过代理工具如Burp Suite)。
3. 使用Console进行动态探测: 有时静态搜索不直观。你可以直接在Console里进行测试。
// 测试 location.hash 是否被使用
location.hash = ‘#test’;
setTimeout(() => { console.log(‘检查页面元素或网络请求,看是否有地方处理了#test’); }, 500);
// 监控DOM修改(非常有用!)
// 保存原始方法
const originalInnerHTML = Object.getOwnPropertyDescriptor(Element.prototype, ‘innerHTML’).set;
// 重写 setter,加入日志
Object.defineProperty(Element.prototype, ‘innerHTML’, {
set: function(value) {
console.log(‘innerHTML被设置到:’, this, ‘值:’, value);
// 可以在这里加入debugger语句进行断点调试
// if (value.includes(‘<script>’)) { debugger; }
return originalInnerHTML.call(this, value);
}
});
运行上面的代码后,操作页面,任何对
innerHTML
的赋值都会在控制台打印出来,清晰看到数据和目标元素。
3.2 第二步:构造与测试Payload
找到可疑的源-汇路径后,就需要构造Payload来验证漏洞是否存在。Payload的构造需要根据上下文(Context)来决定。
1. 确定上下文(Context): 数据最终被放入哪里?这决定了你需要闭合什么标签,或者需要如何构造JavaScript。
-
HTML上下文
:数据被放入
innerHTML、outerHTML、document.write等。你需要构造一个完整的HTML标签来执行脚本。-
最简单:
<img src=x onerror=alert(document.domain)> -
需要闭合前标签:如果插入点是
<div> 用户输入 </div>,你可能需要先闭合</div>再开始攻击。
-
最简单:
-
属性上下文
:数据被放入某个HTML标签的属性值里,如
<input value=”用户输入”>。-
你需要先闭合引号,然后添加事件处理器:
” onmouseover=”alert(1) -
最终效果:
<input value=”” onmouseover=”alert(1)”>
-
你需要先闭合引号,然后添加事件处理器:
-
JavaScript上下文
:数据被直接拼接进JavaScript代码字符串中,然后被
eval或作为setTimeout参数执行。-
你需要闭合字符串和语句:假设代码是
var data = ‘用户输入’;,Payload可以是’; alert(1); //。 -
最终效果:
var data = ‘’; alert(1); //’;
-
你需要闭合字符串和语句:假设代码是
2. 使用编码绕过简单过滤: 如果直接输入被拦截或转义了,尝试编码。
-
HTML实体编码
:
<变成<,>变成>。但如果在JS上下文中,这些编码会被解码后拼接,可能仍然有效。或者,如果网站错误地先解码再使用,就可能造成漏洞。 -
JavaScript Unicode转义
:
alert(1)可以写成\u0061\u006c\u0065\u0072\u0074(1)。这在JS字符串中会被解释为原字符。 -
利用浏览器解析差异
:有时标签名、属性名大小写混用或省略引号可以绕过简单的正则匹配。例如
<ScRiPt>alert(1)</sCrIpT>,<img src=x onerror=alert(1)>(注意onerror没有引号)。
3. 利用伪协议
javascript:
:
常用于
location.href
、
iframe.src
、
a.href
等需要URL的地方。
#javascript:alert(document.domain)
?next=javascript:alert(1)
注意事项 :现代浏览器对
javascript:伪协议在直接地址栏输入或某些上下文中的执行限制越来越严格,但在某些动态赋值场景下依然有效。
3.3 第三步:利用工具提升效率
纯手工效率低,我们需要工具辅助。但记住,工具是辅助,核心判断还得靠人。
1. 浏览器扩展:
- DOM Invader (Burp Suite内置浏览器) :这是目前我心中最强的DOM XSS辅助工具,没有之一。它集成在Burp的浏览器中,能自动识别源和汇,并尝试自动构造Payload,可视化展示数据流,极大提升了审计效率。
- PPScan :一款国产的浏览器插件,能被动扫描当前页面,提示潜在的DOM XSS风险点,适合在浏览网站时进行初步感知。
2. 静态代码分析工具(SAST):
-
Semgrep
:可以编写自定义规则来搜索JavaScript代码中的源-汇模式。例如,一条规则可以查找所有
innerHTML的赋值,并检查其右侧表达式是否包含来自location的属性。 - CodeQL :更强大也更复杂。它可以为整个代码库建立数据流图,精准地追踪从用户输入点到危险函数的完整路径。适合对大型、重要的前端项目进行深度安全审计。
3. 动态分析工具(DAST/IAST):
- Burp Suite Active Scan :新版Burp的主动扫描引擎已经具备了一定的DOM XSS检测能力,它会使用内置浏览器执行JavaScript并分析DOM变化。
- Arachni、ZAP :这些开源扫描器也逐步在集成基于浏览器的扫描插件,可以尝试。
实操心得 :不要迷信工具的“未发现漏洞”报告。工具擅长找模式,但复杂的业务逻辑、需要多步交互触发的DOM XSS,还是得靠人工分析。我通常的工作流是:先用工具(如DOM Invader)快速扫一遍,标记出高风险点,然后针对这些点,结合业务逻辑进行深度手工测试。
4. 高级利用技巧与绕过手段
当你掌握了基础挖掘方法后,这些高级技巧能让你在漏洞利用上更上一层楼,特别是在面对有一定防护的网站时。
4.1 利用 AngularJS 等框架的客户端模板注入
如果网站使用旧版本的AngularJS(1.x)且未使用
ng-app
指令的
strict
模式,就可能存在客户端模板注入,这本质上是另一种形式的DOM型XSS。
原理
:AngularJS的模板语法
{{ expression }}
会在客户端被解析执行。如果用户输入被直接拼接进模板,且表达式可控,就能执行任意JavaScript。
<!-- 假设搜索框内容可控,并直接用于渲染 -->
<div> 搜索结果显示: {{ userInput }} </div>
Payload
:
{{ constructor.constructor(‘alert(1)’ )() }}
这个Payload利用了AngularJS沙箱逃逸的技术(旧版本沙箱存在漏洞),直接调用JavaScript的
Function
构造函数执行代码。
检测方法 :
- 寻找使用了AngularJS的页面。
-
测试所有输入点,输入
{{ 7*7 }}。如果页面显示49,说明存在模板注入。 -
尝试更复杂的Payload进行利用。可以使用现成的工具如
AngularJS-sandbox-escape-cheatsheet中的Payload。
4.2 基于
postMessage
的跨域XSS
window.postMessage()
是HTML5用于跨窗口/跨域通信的API。如果消息接收方(
message
事件监听器)对消息来源(
event.origin
)验证不严,且直接将消息内容用于危险汇点,就可能造成XSS。
攻击场景 :
-
网站A(
https://vulnerable.com)内嵌了一个来自可信域https://trusted.com的iframe B。 -
B页面包含以下代码:
window.addEventListener(‘message’, function(event) { // 漏洞:没有严格验证 origin! // if (event.origin !== ‘https://trusted.com’) return; document.getElementById(‘display’).innerHTML = event.data; }); -
攻击者可以创建一个恶意页面C,通过
iframe.contentWindow.postMessage()向A页面中的B iframe发送消息。由于A和C同源(或B未验证origin),消息会被接收。 -
攻击者发送的消息
event.data包含恶意HTML/JS,被B页面直接innerHTML,导致XSS在vulnerable.com的上下文中执行。
挖掘方法
:
在目标页面中搜索
addEventListener(‘message’)
或
window.onmessage
。检查其回调函数中:
-
是否对
event.origin进行了严格校验?(应使用白名单比对,而非黑名单或简单的indexOf检查)。 -
是否将
event.data直接用于innerHTML、eval等危险操作?
4.3 绕过常见的防御措施
现代前端框架和库内置了一些防护,但配置不当或版本过旧仍可绕过。
1. 绕过HTML编码:
如果服务器对输入进行了HTML实体编码(如
<
变成
<
),但在前端,JavaScript又错误地进行了解码(例如使用了
innerHTML
赋值,而浏览器会自动解码实体),那么编码就失效了。关键是要找到这种“编码-解码”不一致的点。
2. 利用
<svg>
、
<math>
等标签:
在某些防护规则或WAF中,可能只过滤了
<script>
、
<img>
等常见标签,但忽略了
<svg>
或HTML5的
<math>
标签。这些标签内部可以包含事件处理器或
<script>
标签。
<svg onload=alert(1)>
<svg><script>alert(2)</script></svg>
3. 利用JavaScript协议和动态属性:
对于属性上下文,如果过滤了空格和引号,可以尝试利用Tab符(
%09
)、换行符(
%0a
)进行分隔,或者利用HTML属性可以不用引号的特性(在严格模式下不推荐,但浏览器可能支持)。
”onmouseover=alert(1)
(无空格,依赖浏览器自动解析)
4. 利用
document.domain
降级与跨域:
这是一个相对古老的技巧,但在某些特定子域名配置下可能有效。如果两个页面将
document.domain
设置为相同的父域,它们就可以绕过同源策略进行通信。如果其中一个页面存在XSS,可能会影响到另一个。
5. 防御策略与安全编码实践
知道了如何攻击,才能更好地防御。如果你是开发者,这部分是你的必修课。
5.1 原则:严格实施输入验证与输出编码
这是防御所有XSS的黄金法则,对DOM型XSS同样至关重要,只是执行侧重点在客户端。
1. 输入验证(白名单原则): 在数据从“源”取出后,立即根据其预期用途进行严格验证。
-
预期是数字
:使用
parseInt()或Number()转换,并检查isNaN()。 -
预期是有限的枚举值
(如排序方式
asc/desc):与白名单数组比对。 - 预期是简单文本 :使用正则表达式移除所有非预期字符(如只保留字母、数字、空格)。
// 不好的做法:直接使用
const userInput = location.hash.substring(1);
// 好的做法:验证
const userInput = location.hash.substring(1);
if (!/^[a-zA-Z0-9\s-]+$/.test(userInput)) {
userInput = ‘’;
}
2. 输出编码(根据上下文!): 在数据最终被写入“汇”之前,根据其所在的 上下文 进行编码。这是最关键的一步,用错编码等于没防。
-
HTML上下文
:对
<,>,&,”,’进行HTML实体编码。-
可以使用
textContent或innerText属性代替innerHTML,它们会自动进行文本编码。 -
如果必须使用
innerHTML,务必对动态内容进行编码。可以使用document.createTextNode()创建文本节点,或使用可靠的库如DOMPurify。
-
可以使用
- 属性上下文 :对属性值中的引号进行编码。通常使用HTML实体编码即可,但要注意属性值外的引号。
-
JavaScript上下文
:这非常危险,应尽量避免将动态数据拼接进JS代码。如果必须,需进行JavaScript Unicode转义。
-
绝对不要使用
eval()。 -
使用
JSON.parse()解析数据,而非eval()。 -
使用
addEventListener绑定事件,而非onclick=”…”属性。
-
绝对不要使用
5.2 使用安全的API和框架
1. 选择安全的API:
-
element.textContent/innerText>innerHTML/outerHTML -
element.setAttribute(‘data-xxx’, value)> 拼接字符串后设置innerHTML -
document.createElement()+appendChild()>innerHTML -
addEventListener>onclick等HTML属性
2. 利用现代前端框架的安全特性:
-
React
:默认会对所有在JSX中渲染的变量进行转义。只有使用
dangerouslySetInnerHTML时才有风险,务必对其内容进行净化。 -
Vue
:默认的模板语法(
{{ }})也会进行HTML转义。只有使用v-html指令时才有风险,务必对其内容进行净化。 -
Angular
:默认对所有插值表达式和属性绑定进行净化。使用
bypassSecurityTrust系列方法时要极度谨慎。
3. 使用专业的净化库: 当业务确实需要渲染富文本HTML时(如博客评论、CMS后台),必须使用净化库。
- DOMPurify :目前业界最推荐、最安全的HTML净化库。它只允许安全的HTML元素和属性通过,能有效防御XSS。
const cleanHTML = DOMPurify.sanitize(userControlledHTML);
document.getElementById(‘output’).innerHTML = cleanHTML;
5.3 部署内容安全策略
CSP (Content Security Policy) 是防御XSS的终极武器之一。它通过HTTP头告诉浏览器,哪些来源的资源(脚本、样式、图片等)是可以加载和执行的。
一个严格的CSP能极大地限制XSS的影响,甚至完全阻止它。
Content-Security-Policy: default-src ‘self’; script-src ‘self’ https://trusted.cdn.com; object-src ‘none’;
这个策略意味着:
-
default-src ‘self’:默认只允许加载同源资源。 -
script-src ‘self’ https://trusted.cdn.com:脚本只能从同源或指定的CDN加载。 内联脚本(如<script>alert(1)</script>)和javascript:伪协议都将被阻止 。 -
object-src ‘none’:禁止加载<object>,<embed>,<applet>等,封堵一些老的攻击向量。
CSP对DOM型XSS的防御
:即使攻击者成功注入了
<script>
标签或事件处理器,如果CSP禁止了内联脚本执行,这些代码也不会被浏览器执行。但请注意,CSP配置复杂,需要仔细测试,否则可能影响网站正常功能。可以使用
Content-Security-Policy-Report-Only
头在只报告不拦截的模式下先运行一段时间。
6. 常见问题与排查技巧实录
在实际挖掘和修复过程中,你会遇到各种奇怪的问题。这里记录一些我踩过的坑和解决方法。
6.1 为什么我的Payload不弹窗?
这是新手最常见的问题。别慌,按步骤排查:
- 检查控制台(Console) :首先看有没有JavaScript报错。可能是语法错误,或者因为CSP被拦截。CSP违规信息通常会在控制台显示。
- 检查网络请求 :打开开发者工具的“网络”(Network)标签页,看看Payload是否真的被发送到了你期望的端点?对于DOM型XSS,可能根本不发送请求。
-
使用
alert(1)还是alert(document.domain)? :优先使用alert(document.domain)。这能证明脚本是在目标网站的源下执行的,而不仅仅是触发了浏览器扩展或本地脚本。如果alert(1)弹窗但alert(document.domain)不弹,可能触发了同源策略限制或脚本执行环境有问题。 -
查看实时DOM
:在“元素”(Elements)面板,检查你的Payload是否被正确插入到DOM树中?是否被HTML编码了(显示为
<)?右键点击可疑元素,选择“编辑为HTML”,看看原始代码。 -
调试JavaScript
:在“源代码”(Sources)面板,在你怀疑的“汇”函数(如
innerHTML赋值行)上打上断点。重新触发漏洞,看看程序是否执行到这里?赋值的数据是什么?是否被修改或截断?
6.2 如何处理复杂的JavaScript代码流?
当代码经过压缩、混淆,或者调用链路很长时,手动追踪非常困难。
-
使用浏览器的调试器“调用堆栈”(Call Stack)
:当你在汇点(如
innerHTML赋值)打上断点并触发后,查看调用堆栈。它能显示是哪个函数调用了当前函数,一层层回溯,帮你理清数据流的来龙去脉。 -
使用
console.trace():如果你能修改代码(比如在测试环境),可以在关键的变量赋值处插入console.trace(),它会在控制台打印出当前的调用栈轨迹。 - 关注事件监听器 :很多DOM操作是由事件(点击、输入、加载)触发的。在“源代码”面板的“事件监听器断点”中,勾选“脚本” -> “事件监听器”,然后触发页面动作,调试器会在事件处理函数开始时暂停。
6.3 漏洞修复后如何验证?
修复DOM型XSS后,不能简单测试一下原来的Payload不弹窗就完事。
- 回归测试 :确保原来的攻击向量确实被阻断。使用之前所有成功的Payload进行测试。
-
测试边界情况
:
- 输入超长字符串。
- 输入包含各种空白字符(Tab, 换行)。
- 输入Unicode字符、emoji。
- 尝试不同的编码方式。
-
审查修复代码
:确认修复是采用了“输出编码”还是“输入验证”?编码是否匹配上下文?白名单验证是否足够严格?有没有可能通过其他源(如
document.referrer)绕过? - 使用自动化工具扫描 :用之前提到的DOM Invader或CodeQL对修复后的代码再次进行扫描,看是否引入了新的问题或遗漏了其他路径。
6.4 速查表:常见漏洞模式与Payload
下表汇总了常见的危险模式、测试Payload和修复建议,方便你在审计时快速对照。
| 漏洞模式 | 危险代码示例 | 测试Payload (示例) | 修复建议 |
|---|---|---|---|
innerHTML
/
outerHTML
|
div.innerHTML = userInput;
|
<img src=x onerror=alert(document.domain)>
|
使用
textContent
或净化库(如DOMPurify)
|
document.write
|
document.write(‘Welcome ‘ + name);
|
</script><script>alert(1)</script>
| 避免使用,改用DOM操作方法 |
eval
|
eval(‘var data = “‘ + input + ‘“;’);
|
”; alert(1); //
|
绝对禁止使用
,用
JSON.parse()
等替代
|
setTimeout
/
setInterval
(字符串参数)
|
setTimeout(input, 1000);
|
alert(1);
|
传入函数引用,而非字符串:
setTimeout(function(){…}, 1000)
|
location.href
跳转
|
location.href = redirectUrl;
|
javascript:alert(1)
|
验证URL协议,只允许
http://
或
https://
|
location.hash
直接使用
|
eval(location.hash.substr(1));
|
#alert(1)
| 避免直接执行,对hash值进行严格验证 |
jQuery
.html()
/
.append()
|
$(‘#div’).html(userContent);
|
同
innerHTML
|
使用
.text()
方法,或确保传入内容是安全的
|
| AngularJS 模板注入 |
<div>{{ userContent }}</div>
|
{{ constructor.constructor(‘alert(1)’)() }}
|
升级AngularJS,使用
$sce
服务严格过滤,或迁移到新版Angular
|
这条路从理解原理到熟练挖掘,需要大量的练习和代码阅读。我最开始也是对着
alert(1)
弹窗兴奋不已,后来才慢慢学会追踪复杂的代码流,理解框架特性,甚至写出自己的模糊测试脚本。安全是一个攻防对抗的领域,DOM型XSS因其客户端特性,在这场对抗中占据着一个独特而重要的位置。希望这篇长文能成为你手边一份可靠的指南,下次再遇到看似“干净”的页面时,你会知道从哪里入手,去发现那些隐藏在水面之下的风险。记住,最重要的不是记住所有Payload,而是培养那种追踪数据流、理解代码意图的“侦探”思维。

356

被折叠的 条评论
为什么被折叠?



