跨站脚本攻击(Cross-Site Scripting,简称 XSS)是一种代码注入攻击,攻击者将恶意脚本注入到可信网站的页面中,当其他用户访问该页面时,恶意脚本会在他们的浏览器中执行。之所以叫 "Cross-Site",是因为早期这种攻击主要用于从另一个网站窃取数据,但随着技术发展,XSS 的含义已经扩展到任何将脚本注入网页的攻击。
XSS 的核心原理很简单:网站把用户输入的数据当作代码执行了。比如一个搜索页面会把你输入的关键词回显在结果页上("你搜索的是:xxx"),如果网站没有对关键词做任何过滤,攻击者可以输入 <script>alert(1)</script>,这段脚本就会被浏览器当作页面的一部分执行。
根据恶意脚本的存储位置和触发方式,XSS 主要分为三类:
| 类型 | 存储位置 | 触发条件 | 危害等级 |
|---|---|---|---|
| 反射型 XSS | URL 参数中 | 用户点击恶意链接 | 中 |
| 存储型 XSS | 服务器数据库 | 所有访问页面的用户 | 高 |
| DOM 型 XSS | 客户端 DOM | JS 操作 DOM 时触发 | 中高 |
三种类型的本质区别在于恶意脚本走哪条路到达受害者的浏览器:反射型走 URL(一次性),存储型走数据库(持久化),DOM 型走客户端 JavaScript(不经过服务器)。下面我们逐个深入讲解。
反射型 XSS(Reflected XSS)是最常见也最简单的 XSS 类型。恶意脚本藏在 URL 的参数里,当用户访问这个 URL 时,服务器把参数中的内容"反射"到页面上,浏览器解析页面时就执行了恶意脚本。
之所以叫"反射",是因为恶意代码就像一面镜子——从 URL 进去,从页面出来,只是经过服务器反射了一下,并不会被存储下来。所以反射型 XSS 也叫"非持久型 XSS"。
反射型 XSS 最常出现在以下场景:
下面用 DVWA 的 XSS reflected 模块来演示。假设靶机地址是 192.168.1.100,low 安全级别下,访问:
如果页面弹出了 1 的提示框,说明存在反射型 XSS 漏洞。name 参数的值被直接输出到了 HTML 中,浏览器把 <script> 标签当作了页面的一部分来执行。
反射型 XSS 的完整攻击链需要三步:
反射型 XSS 的弱点在于必须诱导用户点击。攻击者通常会把恶意 URL 伪装成正常链接,通过邮件、社交媒体、短信等方式发送给受害者。由于 URL 中包含目标网站的域名,受害者往往会放松警惕。
<script> 编码后变成 %3Cscript%3E。浏览器会自动解码并执行,对攻击效果没有影响,但能更好地隐藏攻击意图。存储型 XSS(Stored XSS)是危害最大的 XSS 类型。恶意脚本被提交到服务器并存储在数据库中,当其他用户访问包含这些数据的页面时,脚本会自动执行。不需要诱导点击,不需要构造特殊 URL——只要正常浏览页面就会中招。
存储型 XSS 也叫"持久型 XSS",因为恶意代码会一直存在于服务器上,直到被清理。它就像一颗定时炸弹,埋在数据库里,每一个访问对应页面的用户都会触发它。
任何用户输入会被存储并展示给其他用户的地方,都可能存在存储型 XSS:
以 DVWA 的 XSS stored 模块为例。在 low 级别下,在 Guestbook 的留言框中输入:
提交留言后,这段脚本就被存入了数据库。之后任何访问留言板的用户,页面加载时都会执行这段脚本,弹出 "XSS!" 提示框。与反射型不同,不需要每次都构造特殊 URL——一次注入,永久生效。
| 对比维度 | 反射型 XSS | 存储型 XSS |
|---|---|---|
| 攻击范围 | 点击链接的个别用户 | 所有访问页面的用户 |
| 隐蔽性 | URL 可能引起怀疑 | 正常浏览即可触发,极难察觉 |
| 持久化 | 一次性,关闭页面即失效 | 持续存在,直到被清除 |
| 社会工程学 | 需要诱导用户点击 | 无需用户操作,自动触发 |
在真实世界中,存储型 XSS 曾多次造成大规模安全事件。比如某知名论坛的用户签名处存在存储型 XSS,攻击者注入后,每一个看到该用户帖子的人都会被窃取 Cookie,一夜之间成千上万的账号被盗。
DOM 型 XSS(DOM-based XSS)是三种 XSS 中最特殊的一种。它的恶意脚本完全在客户端执行,不经过服务器——服务器返回的页面源代码是干净的,但页面中的 JavaScript 把 URL 中的数据读取出来,直接写入 DOM,导致脚本执行。
DOM(Document Object Model)是浏览器对 HTML 页面的结构化表示。当 JavaScript 使用 document.write()、innerHTML 等方法把用户可控的数据写入页面时,如果数据中包含 HTML 标签,浏览器就会解析并执行它们。
DOM 型 XSS 常见于以下场景:
下面是一个最简单的 DOM 型 XSS 示例。假设页面中有这样一段 JavaScript:
当访问 http://example.com/page.html#<img src=x onerror=alert(1)> 时,JavaScript 从 hash 中读取内容,用 innerHTML 写入页面,<img> 标签被浏览器解析,触发 onerror 事件执行脚本。
理解三种 XSS 最直观的方式,是看恶意代码的"旅行路径":
DVWA 的 XSS DOM 模块就是一个很好的练习靶场。它使用前端 JavaScript 读取 URL 中的 default 参数来设置下拉框的默认值,low 级别下没有任何过滤。你可以尝试构造一个利用 URL hash 的 payload 来触发弹窗。
初学者接触 XSS 时,往往只停留在 alert(1) 弹窗的层面。实际上 XSS 的威力远不止于此——只要能在目标网站的上下文中执行 JavaScript,攻击者就能做用户能做的几乎任何事情。下面介绍几种常见的利用方式。
这是 XSS 最经典的利用方式。网站通常用 Cookie 来识别用户身份,如果攻击者能获取到受害者的 Cookie,就可以冒充受害者登录网站,这就是"会话劫持"。
这段脚本会把用户的 Cookie 作为参数发送到攻击者的服务器。攻击者拿到 Cookie 后,就可以在自己的浏览器中设置相同的 Cookie 值,从而以受害者的身份访问网站。
不过,现代浏览器的 Cookie 通常会设置 HttpOnly 标志,设置了 HttpOnly 的 Cookie 无法被 JavaScript 读取,能有效抵御 Cookie 窃取。但这并不意味着 XSS 就没用了——还有很多其他利用方式。
XSS 可以用来在可信网站上注入伪造的登录框或弹窗,诱导用户输入账号密码。由于钓鱼界面是在真实网站的域名下显示的,用户很难察觉。
用户看到的是一个遮罩层加登录框,看起来就像网站要求重新验证身份。由于地址栏显示的是真实网站的域名,即使是谨慎的用户也可能被骗输入密码。
XSS 的想象力边界取决于 JavaScript 的能力边界。只要能在目标域执行脚本,攻击者就可以利用浏览器 API 做很多事情。这也是为什么 XSS 长期占据 OWASP Top 10 榜单前列。
在渗透测试中,检测 XSS 的基本思路是:在每个输入点插入测试字符串,然后观察输出位置是否被正确过滤。但真实场景中,网站往往有各种过滤机制——黑名单、HTML 实体编码、WAF 等。这时候就需要掌握绕过技巧。
检测 XSS 的第一步不是直接塞 <script>,而是先搞清楚输入的内容出现在页面的什么位置。不同的输出位置,构造 Payload 的方式完全不同:
| 输出位置 | 测试思路 | 示例 Payload |
|---|---|---|
| HTML 标签之间 | 注入新标签 | <script>alert(1)</script> |
| HTML 属性值中 | 闭合属性 + 事件 | " onclick="alert(1) |
| JavaScript 代码中 | 闭合字符串 + 执行 | ';alert(1);// |
| CSS 样式中 | expression / url | expression(alert(1)) |
一个好的习惯是先输入一个独特的测试字符串(比如 xssTest123),然后在页面源代码中搜索这个字符串,看它出现在什么位置、周围是什么上下文,再决定用什么方式注入。
当 <script> 被过滤时,可以尝试以下替代方案:
<img src=x onerror=alert(1)>、<svg onload=alert(1)> 等事件触发
<SCRIPT> 或 <ScRiPt> 绕过
script,用 <scrscriptipt> 绕过(移除中间的 script 后变成 script)
href、src 等属性中使用 javascript:alert(1)
了解攻击是为了更好地防御。XSS 的防御是一个多层次的体系,单一措施往往不够,需要多层防护叠加才能有效降低风险。
XSS 防御的根本原则是:根据数据插入的上下文进行对应的编码。同一条数据,插入到 HTML 正文、HTML 属性、JavaScript 字符串、CSS、URL 中,需要的编码方式完全不同。
| 上下文 | 编码方式 | 编码示例 |
|---|---|---|
| HTML 正文 | HTML 实体编码 | < > & |
| HTML 属性 | 属性值编码 + 引号 | " ' |
| JavaScript | JS Unicode 编码 | \u003c \x3c |
| URL 参数 | URL 编码 | %3C %3E %26 |
| CSS | CSS 十六进制编码 | \3C \3E |
现代框架(React、Vue、Angular 等)默认会对模板中的变量进行 HTML 转义,大大降低了 XSS 风险。但如果使用了 dangerouslySetInnerHTML、v-html 等绕过转义的 API,XSS 风险依然存在。
除了输出编码,还有两道重要的防线:
一个严格的 CSP 示例:Content-Security-Policy: default-src 'self'; script-src 'self'。这条策略只允许加载同源的脚本和资源,内联脚本和 eval 都会被阻止。
XSS 是一种强大的攻击手段,但也是法律风险极高的行为。在学习和实践中,必须严格遵守以下原则:
根据《中华人民共和国刑法》第二百八十五条,非法侵入计算机信息系统或非法获取计算机信息系统数据,情节严重的可处三年以下有期徒刑。学习网络安全技术的目的是保护系统安全,而不是破坏它。
"Hello XSS" 的提示框。提示:直接在 name 参数中输入 script 标签即可。<script> 标签)。请尝试至少两种不使用 script 标签的方式触发 XSS。提示:事件属性、其他 HTML 标签。script-src 'self'),观察 CSP 是否能阻止这个 XSS。提示:可以用 Node.js 或 Python 快速搭建测试服务器。