DOM Based Cross Site Scripting(XSS)DOM 型跨站脚本漏洞:前端 URL 解析缺陷与 DVWA 分级绕过实战
2026/8/19 12:05:31 网站建设 项目流程

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.hashdocument.referrerwindow.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()innerHTMLouterHTML等危险 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=userInput
element.outerHTML=userInput
eval(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>");}

代码逻辑拆解

  1. document.location.href

前端直接获取当前浏览器完整 URL,URL 中的default参数完全由访问者控制。

  1. substring(... + 8)

代码从default=后面开始截取内容,并将剩余字符串保存到变量lang

例如:

?default=English

最终:

lang="English"
  1. 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 级别使用switchdefault参数进行白名单校验:

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

攻击流程:

  1. HTTP请求携带参数default=English#后的payload不参与网络传输;
  2. PHP白名单校验通过,正常返回页面,无重定向;
  3. 浏览器拿到页面,JS读取完整location.href,截取default=后的全部内容,包含#以及后面的恶意字符串;
  4. 恶意payload传入document.write(),解析为HTML,触发onerror执行JS。

关键结论:

  1. 服务端对$_GET['default']的白名单防护本身完备;
  2. 漏洞根源是前端错误读取完整href,服务端完全感知不到#后的本地片段;
  3. 服务端校验无法保护仅存于浏览器Hash片段的数据,危险Sinkdocument.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 级别安全总结

  1. 不再将 URL 编码字符恢复为 HTML 标签字符;
  2. 恶意内容无法从编码文本转换为真实 DOM 结构;
  3. 前端的 URL 数据流在进入document.write()前失去可执行 HTML 语义;
  4. 对当前 DVWA 页面和当前 Payload 而言,DOM XSS 执行链被阻断。

不过从真实开发角度看,更推荐的做法仍然是:

不要把不可信数据拼接进document.write(),而是使用textContentcreateElement()等安全 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);

textContentcreateElement()不会把输入当成 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,浏览器一样可能执行攻击者控制的内容。


免责声明

  1. 本文所有内容仅用于网络安全技术研究与教学演示,所有测试均在本地授权搭建的 DVWA 靶场环境中进行,旨在帮助读者理解 DOM XSS 漏洞原理与防御机制。

  2. 请勿将本文涉及的技术与方法用于任何未经授权的真实系统测试。任何利用本文内容进行的非法攻击行为,均与作者无关,由使用者自行承担相应法律责任。

  3. 网络安全学习与测试应严格遵守相关法律法规,仅在获得明确授权的前提下开展安全测试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询