DOM Based Cross Site Scripting(XSS)DOM 型跨站脚本漏洞:前端 URL 解析缺陷与 DVWA 分级绕过实战

前言

今天继续 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><img src=1 onerror=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) {
    var lang = 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() 写入页面。

当输入是普通语言名称时,最终结构类似:

<option value='English'>English</option>

但当输入中包含可以闭合原标签、再插入新标签的内容时,浏览器就会把它解析为新的 DOM 结构。

漏洞根源概括

用户可控的 URL 参数未经 HTML 编码,直接进入 document.write() 这个危险 DOM Sink。虽然服务端没有直接输出 Payload,但前端脚本重新把它拼成了 HTML,DOM XSS 成功触发。

标准修复方式

不要使用 document.write() 拼接不可信 HTML。应使用安全 DOM API:

const option = 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 tags
    if (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 languages
    switch ($_GET['default']) {
        case "French":
        case "English":
        case "German":
        case "Spanish":
            # ok
            break;
        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;

或者:

const option = 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. 网络安全学习与测试应严格遵守相关法律法规,仅在获得明确授权的前提下开展安全测试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sagittarius_A*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值