DOM Based Cross Site Scripting(XSS)DOM 型跨站脚本漏洞:前端 URL 解析缺陷与 DVWA 分级绕过实战
- 前言
- 1. DOM Based XSS 核心理论基础
- 1.1 什么是 DOM 型 XSS?
- 1.2 DOM XSS 的本质
- 1.3 DOM XSS 成立的必要条件
- 1.4 DOM XSS、Reflected XSS 与 Stored XSS 的区别
- 1.5 DOM XSS 可能造成什么危害?
- 2. DVWA DOM XSS(Low)
- 2.1 页面黑盒探测
- 2.2 黑盒漏洞利用实操
- 2.3 源码审计:漏洞核心不在 Low.php,而在前端 DOM 拼接
- 3. DVWA DOM XSS(Medium)
- 3.1 页面黑盒探测
- 3.2 黑名单绕过思路
- 3.3 源码审计:只拦截 script 关键字的缺陷
- 4. DVWA DOM XSS(High)
- 4.1 页面黑盒探测
- 4.2 白名单防护为什么更可靠?
- 4.3 源码审计:服务端语言白名单
- 4.4 High级别绕过:URL锚点`#`片段
- 5. DVWA DOM XSS(Impossible)
- 5.1 页面黑盒验证
- 5.2 源码审计:不解码 Query String,阻断 DOM 注入
- 6. DOM XSS 防护的正确姿势
- 6.1 避免使用危险 DOM Sink
- 6.2 Source 与 Sink 之间必须做安全处理
- 6.3 不要依赖黑名单过滤
- 6.4 白名单与 CSP 作为纵深防御
- 7. DVWA DOM XSS 四个等级对比
- 8. 总结
- 免责声明
前言
今天继续 DVWA Web 漏洞靶场系列,来学习 XSS 漏洞中最容易被初学者忽略、也最容易被“后端没问题”这种想法误判的一类漏洞——DOM Based Cross Site Scripting(DOM 型跨站脚本,简称 DOM XSS)。
如果说反射型 XSS 是服务端把用户输入反射到页面,存储型 XSS 是服务端把恶意内容保存后再展示,那么 DOM XSS 的危险点更靠近浏览器本身。
它甚至不一定需要服务端把恶意内容写进 HTML 响应。
攻击者构造的内容到达浏览器后,前端 JavaScript 再从 URL、location.hash、document.referrer、window.name等位置读取数据,并使用不安全的 DOM API 拼接回页面。整个危险过程可能完全发生在浏览器端。
如果说前两类 XSS 的典型链路是:
用户输入 → 服务端处理错误 → 浏览器执行那么 DOM XSS 的典型链路则是:
用户输入 → 前端 JavaScript 处理错误 → DOM 被污染 → 浏览器执行这也是 DOM XSS 最容易被忽略的原因:
服务端日志里可能看不到完整的危险内容,后端源码也可能几乎没有漏洞逻辑,但浏览器里的 JavaScript 仍然能把 URL 参数重新拼成可执行 HTML。
DVWA 的 DOM XSS 靶场通过一个“语言选择”下拉框,展示了 URL 中的default参数如何被前端脚本读取,并动态写入页面。Low、Medium、High、Impossible 四个等级则分别展示了无防护、黑名单过滤、服务端白名单和客户端不解码的差异。
本文前面的测试依旧严格采用黑盒思路:先观察页面、先改参数、先看浏览器现象;源码为什么会这样,放到每个等级的最后再审计。
1. DOM Based XSS 核心理论基础
1.1 什么是 DOM 型 XSS?
DOM 型跨站脚本漏洞(DOM Based Cross Site Scripting),是一类主要由前端 JavaScript 操作 DOM 时引入的 XSS 问题。
它的典型特征是:
- 攻击者控制 URL 参数、Hash、Referer 等浏览器侧数据;
- 前端 JavaScript 从这些位置读取内容;
- JavaScript 使用
document.write()、innerHTML、outerHTML等危险 API 将内容写回页面; - 浏览器将攻击者输入当成 HTML 结构解析;
- 恶意脚本在当前站点上下文中执行。
例如,一个页面从 URL 中读取语言参数:
?default=English前端脚本本来只想让下拉框显示当前语言:
English但如果 JavaScript 没有把参数当作普通文本处理,而是直接拼接进 HTML 字符串,攻击者就可能通过构造特殊default参数,让浏览器解析出额外的 HTML 标签。
1.2 DOM XSS 的本质
DOM XSS 的漏洞链路可以概括为:
攻击者可控 URL 参数 ↓ 浏览器加载目标页面 ↓ 前端 JavaScript 读取 location.href / location.hash 等数据源 ↓ 输入未编码就进入 document.write / innerHTML 等危险 DOM API ↓ 浏览器重新解析 DOM 结构 ↓ 恶意 HTML 或 JavaScript 执行最关键的问题不是“URL 里有没有<script>”,而是:
前端代码把 URL 中的不可信字符串,当成了可以直接拼接进 HTML 的内容。
开发者本来只是想动态生成一个下拉框选项,但如果使用字符串拼接配合document.write(),浏览器最终接收到的就不再只是一个语言名称,而可能是一段攻击者控制的 HTML。
1.3 DOM XSS 成立的必要条件
DOM XSS 一般需要满足以下几个条件。
条件一:存在客户端可控的数据源(Source)
常见 Source 包括:
document.location.href;location.search;location.hash;document.referrer;window.name;postMessage接收的数据;- 浏览器本地存储中的数据。
条件二:前端脚本将数据写入危险位置(Sink)
常见危险 Sink 包括:
document.write()element.innerHTML=userInputelement.outerHTML=userInputeval(userInput)setTimeout(userInput,0)条件三:Source 到 Sink 的数据流没有进行安全处理
也就是说,URL 中的参数被前端读取后,没有使用安全 DOM API 作为普通文本写入,而是直接参与 HTML 字符串拼接。
1.4 DOM XSS、Reflected XSS 与 Stored XSS 的区别
| 类型 | 危险数据主要在哪里处理 | 恶意内容是否持久化 | 典型特点 |
|---|---|---|---|
| Reflected XSS | 服务端反射响应 | 通常不保存 | 服务端将参数回显到当前页面 |
| Stored XSS | 服务端保存后再输出 | 保存 | 后续访问者可能持续触发 |
| DOM XSS | 浏览器前端 JavaScript | 不一定保存 | 前端读取 URL / Hash 后污染 DOM |
反射型 XSS 的常见流程:
恶意参数 → 服务端拼接响应 → 浏览器执行存储型 XSS 的常见流程:
恶意内容 → 服务端保存 → 后续页面回显 → 浏览器执行DOM XSS 的常见流程:
恶意参数 → 浏览器 URL → 前端脚本读取 → DOM 拼接 → 浏览器执行所以 DOM XSS 的审计重点不只是 PHP、Java、Python 等后端代码,还必须看浏览器侧 JavaScript 的数据流。
1.5 DOM XSS 可能造成什么危害?
DOM XSS 的最终执行环境依旧是受害者浏览器,因此危害与其他 XSS 类型类似:
- 篡改页面内容,伪造登录、支付或客服界面;
- 诱导用户输入敏感信息;
- 读取当前页面中可访问的 DOM 数据;
- 诱导用户执行站内操作;
- 影响使用同一页面的普通用户或管理员;
- 配合钓鱼、点击劫持等手段扩大影响。
不同的是,DOM XSS 很容易被开发者忽略。
后端可能只看到一次看似正常的页面请求,真正把恶意内容变成 HTML 的逻辑却发生在用户自己的浏览器里。
2. DVWA DOM XSS(Low)
2.1 页面黑盒探测
进入 DVWA:
Vulnerabilities → XSS (DOM)页面提供一个语言下拉框,正常情况下可以选择:
English French Spanish German先选择:
English点击提交后,观察浏览器地址栏。
页面 URL 会出现类似参数:
?default=English此时下拉框顶部会显示当前传入的语言内容。
接下来,直接修改地址栏中的default参数:
?default=TestLanguage如果刷新后,下拉框中出现了一个新的TestLanguage选项,而页面并没有提示“语言不存在”或“参数非法”,说明前端正在读取 URL 参数,并把它动态写进页面。
这里已经出现了一个典型的 DOM XSS 信号:
URL 参数改变后,页面结构跟着发生变化,但并没有看到传统的服务端回显区域。
2.2 黑盒漏洞利用实操
DOM XSS 的关键在于确认default参数是否能从“普通文本”突破成“HTML 结构”。
DVWA 这个页面的参数最终会出现在下拉框的<option>位置,因此可以先构造一个闭合原有option标签、再插入新标签的无害测试内容:
?default=English%3C%2Foption%3E%3Cimg%20src%3D1%20onerror%3Dalert(document.domain)%3E为了方便理解,URL 解码后的核心内容是:
English</option><imgsrc=1onerror=alert(document.domain)>访问该 URL 后,如果浏览器弹出当前站点域名,说明 URL 参数已经被前端 JavaScript 拼接进页面,并被浏览器当成 HTML 解析。
这里没有提交表单,没有写入数据库,也没有在当前页面看到明显的 PHP 回显。
攻击链完全发生在浏览器端:
攻击者构造 default 参数 ↓ 浏览器加载页面 ↓ 页面脚本读取当前 URL ↓ 脚本将参数拼接进下拉框 HTML ↓ 浏览器解析新增 HTML ↓ 事件属性触发 JavaScript这就是典型的 DOM Based XSS。
2.3 源码审计:漏洞核心不在 Low.php,而在前端 DOM 拼接
Low 级别的后端源码只有:
<?php# No protections, anything goes?>从 PHP 角度看,Low.php 几乎没有任何处理逻辑。
这正是 DOM XSS 容易让人误判的地方:如果只盯着后端漏洞源码,会发现“这里什么都没有”。
真正的危险代码在页面主文件生成的前端 JavaScript 中:
if(document.location.href.indexOf("default=")>=0){varlang=document.location.href.substring(document.location.href.indexOf("default=")+8);document.write("<option value='"+lang+"'>"+decodeURI(lang)+"</option>");}代码逻辑拆解
document.location.href
前端直接获取当前浏览器完整 URL,URL 中的default参数完全由访问者控制。
substring(... + 8)
代码从default=后面开始截取内容,并将剩余字符串保存到变量lang。
例如:
?default=English最终:
lang="English"document.write()
漏洞关键语句:
document.write("<option value='"+lang+"'>"+decodeURI(lang)+"</option>");程序将lang直接拼接到 HTML 字符串中,再使用document.write()写入页面。
当输入是普通语言名称时,最终结构类似:
<optionvalue='English'>English</option>但当输入中包含可以闭合原标签、再插入新标签的内容时,浏览器就会把它解析为新的 DOM 结构。
漏洞根源概括
用户可控的 URL 参数未经 HTML 编码,直接进入document.write()这个危险 DOM Sink。虽然服务端没有直接输出 Payload,但前端脚本重新把它拼成了 HTML,DOM XSS 成功触发。
标准修复方式
不要使用document.write()拼接不可信 HTML。应使用安全 DOM API:
constoption=document.createElement('option');option.value=lang;option.textContent=lang;select.appendChild(option);textContent会把用户输入当成纯文本,而不是 HTML 标签解析。
3. DVWA DOM XSS(Medium)
3.1 页面黑盒探测
将 DVWA Security 切换为 Medium 后,先重复 Low 级别的测试。
直接在地址栏中传入经典脚本标签:
?default=%3Cscript%3Ealert(document.domain)%3C%2Fscript%3E如果页面没有执行,而是跳转回:
?default=English说明应用程序已经对包含script的输入增加了拦截。
但这里必须明确:
“过滤了
<script>”不等于“修复了 DOM XSS”。
因为 DOM XSS 不只依赖<script>标签,浏览器中还有大量可以进入脚本执行上下文的 HTML 结构。
3.2 黑名单绕过思路
Medium 级别的现象很明显:包含script的输入会被拦截并跳回默认语言。
但我们的 Low 级别 Payload:
?default=English%3C%2Foption%3E%3Cimg%20src%3D1%20onerror%3Dalert(document.domain)%3E并不包含<script>字符串。
访问后,如果浏览器仍然弹出当前域名,说明当前防护只关注了script标签,没有阻止 URL 参数继续进入document.write()。
这就是黑名单防御的典型问题。
开发者只要盯着:
<script>攻击者就可以放弃使用<script>,转而寻找其他可被浏览器解析的标签和事件属性。
从黑盒角度看,Medium 的真实问题并不是“大小写写得不对”,而是:
页面只过滤了一段关键字,却没有阻断 URL 参数到危险 DOM API 的数据流。
3.3 源码审计:只拦截 script 关键字的缺陷
Medium 级别后端 PHP 源码:
<?php// Is there any input?if(array_key_exists("default",$_GET)&&!is_null($_GET['default'])){$default=$_GET['default'];# Do not allow script tagsif(stripos($default,"<script")!==false){header("location: ?default=English");exit;}}?>代码逻辑拆解
页面先检查 GET 请求中是否存在:
default参数,然后将其保存到:
$default=$_GET['default'];接着调用:
stripos($default,"<script")stripos()是不区分大小写的字符串查找函数。
只要输入中包含:
<script函数就会返回匹配位置,程序随即执行:
header("location: ?default=English");exit;也就是说,页面不是对用户输入进行安全编码,而是发现<script后直接重定向回默认语言。
防护存在的致命缺陷
它只拦截script关键字:
if(stripos($default,"<script")!==false)但真正的 DOM 漏洞点依旧存在于前端:
document.write("<option value='"+lang+"'>"+decodeURI(lang)+"</option>");只要 Payload 不含<script,就能避开后端检查,并继续到达浏览器的危险 Sink。
正确修复方案
不要依赖关键字黑名单。真正需要修复的是前端 DOM 拼接逻辑:
option.textContent=lang;或者对写入 HTML 的内容做正确的上下文编码。
4. DVWA DOM XSS(High)
4.1 页面黑盒探测
High 级别不再只是过滤某一个标签,而是对default参数允许的语言范围进行限制。
先正常访问:
?default=English页面可以正常显示 English。
再尝试:
?default=French?default=German?default=Spanish这些预期语言同样可以正常显示。
但如果将default改成任意非语言值,例如:
?default=TestLanguage页面会被跳转回:
?default=English此时再提交 Low / Medium 中使用的 DOM XSS Payload,也会在页面加载前被重定向到 English,无法到达浏览器中的 DOM 拼接位置。
4.2 白名单防护为什么更可靠?
High 级别没有继续扩大“危险标签黑名单”,而是换了一个方向:
只允许已知安全的业务值对于语言选择功能来说,业务真正需要接受的值本来就只有:
English French German Spanish既然合法输入范围非常有限,就没有必要接收任意字符串,再去猜测里面有没有危险标签。
白名单逻辑是:
输入属于允许集合 → 正常处理 输入不属于允许集合 → 直接回到默认值相比黑名单:
输入中不能出现某些危险关键字白名单更贴合业务逻辑,也更容易控制攻击面。
4.3 源码审计:服务端语言白名单
High 级别后端 PHP 源码:
<?php// Is there any input?if(array_key_exists("default",$_GET)&&!is_null($_GET['default'])){# White list the allowable languagesswitch($_GET['default']){case"French":case"English":case"German":case"Spanish":# okbreak;default:header("location: ?default=English");exit;}}?>代码逻辑拆解
High 级别使用switch对default参数进行白名单校验:
case"French":case"English":case"German":case"Spanish":只有这四个固定字符串可以继续进入页面。
其他任何值都会执行:
header("location: ?default=English");exit;也就是说,攻击者构造的 DOM XSS 参数在服务端阶段就被拦截,不会进入后续页面,也就无法被前端 JavaScript 拼接到 DOM 中。
安全逻辑总结
High 级别的安全性来自业务白名单,而不是对所有危险 XSS 语法逐个猜测。
对于“语言选择”这种固定选项功能,白名单是合理而有效的防护方式。
但也要注意:
白名单拦住了当前
default参数,并不代表前端document.write()本身就变安全了。
如果未来该前端代码复用到其他允许自由文本输入的业务场景,危险 DOM Sink 依旧可能重新成为漏洞点。
4.4 High级别绕过:URL锚点#片段
直接通过GET参数?default=传入恶意载荷会被服务端白名单拦截,无法利用。但DVWA的前端JS读取的是完整的document.location.href,而非location.search。
URL中#之后的锚点片段不会发送到后端服务器,仅保存在浏览器本地。后端PHP只能接收到?default=English,白名单校验直接放行,不会触发重定向;但前端JS会读取完整URL,把#后面的恶意内容一并取出送入document.write(),完成DOM‑XSS触发。
构造Payload:
?vulnerabilities/dom_xss/?default=English#%3C%2Foption%3E%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E攻击流程:
- HTTP请求携带参数
default=English,#后的payload不参与网络传输; - PHP白名单校验通过,正常返回页面,无重定向;
- 浏览器拿到页面,JS读取完整
location.href,截取default=后的全部内容,包含#以及后面的恶意字符串; - 恶意payload传入
document.write(),解析为HTML,触发onerror执行JS。
关键结论:
- 服务端对
$_GET['default']的白名单防护本身完备;- 漏洞根源是前端错误读取完整
href,服务端完全感知不到#后的本地片段;- 服务端校验无法保护仅存于浏览器Hash片段的数据,危险Sink
document.write()始终存在。
5. DVWA DOM XSS(Impossible)
5.1 页面黑盒验证
切换到 Impossible 后,继续访问 Low 级别使用的 URL 编码 Payload:
?default=English%3C%2Foption%3E%3Cimg%20src%3D1%20onerror%3Dalert(document.domain)%3E此时页面不会弹窗。
参数中的%3C、%3E等 URL 编码字符不会再被还原成真正的<、>标签字符,而是以编码文本的形式保留在页面中。
这说明 Impossible 并没有继续增加更多后端过滤规则,而是直接从客户端 URL 解码这一环下手,阻断攻击者把编码内容转换成 HTML 标签。
5.2 源码审计:不解码 Query String,阻断 DOM 注入
Impossible 级别的漏洞源码文件本身只有:
<?php# Don't need to do anything, protection handled on the client side?>这句话已经说明了关键:防护逻辑被放在客户端。
页面主文件中会根据安全等级决定是否调用decodeURI():
# For the impossible level, don't decode the querystring$decodeURI="decodeURI";if($vulnerabilityFile=='impossible.php'){$decodeURI="";}页面写入下拉框时使用:
document.write("<option value='"+lang+"'>"+$decodeURI(lang)+"</option>");在 Low、Medium、High 中,页面会调用:
decodeURI(lang)URL 中的:
%3C %3E会被还原为:
< >于是编码后的 HTML 标签重新获得浏览器解析能力。
而 Impossible 级别将$decodeURI置为空,最终不再对 Query String 进行解码。
攻击者传入的%3Cimg...%3E不会恢复为<img...>,浏览器就不会把它当成真实 HTML 标签解析。
Impossible 级别安全总结
- 不再将 URL 编码字符恢复为 HTML 标签字符;
- 恶意内容无法从编码文本转换为真实 DOM 结构;
- 前端的 URL 数据流在进入
document.write()前失去可执行 HTML 语义; - 对当前 DVWA 页面和当前 Payload 而言,DOM XSS 执行链被阻断。
不过从真实开发角度看,更推荐的做法仍然是:
不要把不可信数据拼接进
document.write(),而是使用textContent、createElement()等安全 DOM API。
6. DOM XSS 防护的正确姿势
6.1 避免使用危险 DOM Sink
以下写法风险很高:
document.write(userInput);element.innerHTML=userInput;element.outerHTML=userInput;如果业务只是展示文字,应优先使用:
element.textContent=userInput;或者:
constoption=document.createElement('option');option.value=userInput;option.textContent=userInput;select.appendChild(option);textContent和createElement()不会把输入当成 HTML 标签解析。
6.2 Source 与 Sink 之间必须做安全处理
DOM XSS 审计时,必须同时盯住两类位置:
Source:数据从哪里来Sink:数据最终写到哪里去例如:
location.href ↓ substring() ↓ lang ↓ document.write()只要可控数据从 Source 一路流入危险 Sink,中间没有安全处理,就可能形成 DOM XSS。
6.3 不要依赖黑名单过滤
以下思路都不推荐作为 DOM XSS 的根本修复方案:
if(userInput.indexOf('<script')!==-1){// 拦截}userInput=userInput.replace(/script/gi,'');原因和反射型、存储型 XSS 一样:黑名单无法覆盖浏览器所有可执行 HTML 语法。
真正的修复方向应该是:
不让不可信输入进入 HTML 解析上下文而不是:
不断猜测攻击者会输入什么标签6.4 白名单与 CSP 作为纵深防御
对于 DVWA 中的语言选择功能,High 级别的业务白名单非常合适:
English French German Spanish如果业务输入本来就只允许固定选项,应该优先限制为固定集合。
另外,Content Security Policy(CSP)也可以限制脚本执行来源、减少部分 XSS 的影响。
但必须注意:
CSP 和白名单属于纵深防御,不能替代安全 DOM API 和正确的输出处理。
7. DVWA DOM XSS 四个等级对比
| 安全等级 | 核心防护逻辑 | 防护效果 | 主要问题 |
|---|---|---|---|
| Low | 无服务端过滤,前端直接document.write() | 完全可利用 | URL 参数直接进入危险 DOM Sink |
| Medium | 拦截包含<script的输入 | 可拦截经典脚本标签 | 只过滤关键字,其他 HTML 结构仍可能绕过 |
| High | 服务端语言白名单 | 当前参数可有效阻断 | 前端document.write()仍是潜在危险代码 |
| Impossible | 客户端不再decodeURI()参数 | URL 编码标签不再恢复为 HTML | 更推荐彻底移除字符串拼接式 DOM 写入 |
从 DVWA 的四个等级可以非常直观地看出防护演进:
无防护 ↓ 简单黑名单 ↓ 业务白名单 ↓ 客户端阻断解码链路真正可靠的安全方案,不是把关键字过滤写得越来越复杂,而是:
不要让 URL、Hash 等不可信数据以 HTML 字符串的形式进入
document.write()、innerHTML等危险 DOM API。
8. 总结
DOM XSS 的漏洞原理并不复杂:
攻击者控制浏览器侧数据,前端 JavaScript 又将这些数据直接拼接进 DOM,浏览器最终把它们当成 HTML 或 JavaScript 解析执行。
DVWA 的不同安全等级分别展示了:
- Low:没有任何防护,URL 参数直接进入
document.write(); - Medium:只拦截
<script关键字,无法覆盖其他 HTML 解析入口; - High:通过固定语言白名单阻断非法
default参数; - Impossible:不再解码 Query String,避免 URL 编码内容恢复成可执行 HTML 标签。
最后记住 DOM XSS 最重要的一句话:
后端没有把 Payload 回显,不代表没有 XSS;只要前端 JavaScript 把 URL 中的不可信数据重新写进 DOM,浏览器一样可能执行攻击者控制的内容。
免责声明
本文所有内容仅用于网络安全技术研究与教学演示,所有测试均在本地授权搭建的 DVWA 靶场环境中进行,旨在帮助读者理解 DOM XSS 漏洞原理与防御机制。
请勿将本文涉及的技术与方法用于任何未经授权的真实系统测试。任何利用本文内容进行的非法攻击行为,均与作者无关,由使用者自行承担相应法律责任。
网络安全学习与测试应严格遵守相关法律法规,仅在获得明确授权的前提下开展安全测试。