摘要: XSS(跨站脚本攻击)自 1996 年被发现以来,连续二十余年出现在 OWASP Top 10 名单中,是 Web 安全领域生命力最顽强的漏洞类型之一。本文是系列的上篇,面向完全没有安全基础 的读者,从三个层面建立认知:第一 ,讲清楚 XSS 到底是什么、为什么叫"跨站"而不是"CSS"、反射型/存储型/DOM 型三兄弟的本质区别;第二 ,手把手完成 XSS-Labs 靶场在小皮面板下的部署;第三 ,逐关拆解 Level 1 到 Level 10 的绕过逻辑。读完本篇,你将不再把 XSS 理解为"弹个窗就完事了",而是能够清晰地判断:我的输入落到了 HTML 的哪个位置,浏览器在这个位置会怎样解析它。
一、XSS 到底是什么:一个弹窗背后的二十年
1.1 从一个最简单的场景说起
假设一个网站有一个搜索功能,用户搜索"手机",页面显示"没有找到和手机相关的结果"。如果你搜索的不是"手机",而是一段这样的文字:
<script>alert('hello')</script>
页面原样把它拼进了 HTML 里,浏览器读到 <script> 标签,就会把它当作代码执行,弹出一个框。
这就是 XSS 最原始、最核心的画面: 服务器把用户输入当成了 HTML 结构的一部分输出给浏览器,浏览器忠实地执行了其中的脚本。 攻击者"跨"过原本的站点边界,让受害者的浏览器执行了攻击者指定的 JavaScript。
1.2 为什么叫 XSS 而不是 CSS
跨站脚本攻击的英文是 Cross-Site Scripting,首字母缩写本该是 CSS。但 CSS 已经被"层叠样式表"(Cascading Style Sheets)占用了。为了避免在安全圈里产生歧义,研究者们把它改成了 XSS 。这个名字本身也暗示了 XSS 的关键特征: 脚本来自"别处",却在"当前站点"的上下文里执行。
1.3 XSS 的"前世":它比很多人想象的更老
XSS 不是互联网的"新问题"。公开记录显示,最早的 XSS 攻击概念可以追溯到 1996 年前后,攻击者利用 iframe 等早期 Web 特性在页面之间注入内容。"Cross-Site Scripting"这个术语则大约在 2000 年由微软的安全研究人员正式提出并固定下来。
OWASP(开放式 Web 应用安全项目)从 2003 年开始发布 Top 10 风险名单,XSS 从未缺席:2004 年第 4、2007 年第 4、2010 年第 1、2013 年第 3、2017 年第 7;2021 年 OWASP 将 XSS 归入排名第 3 的"注入(Injection)"类别。一个存在了近三十年的漏洞类型,至今仍然频繁出现在真实系统中,这本身就说明: XSS 的根源不在技术复杂度,而在"把用户输入当代码"这个反复被低估的风险。
1.4 反射型、存储型、DOM 型:三兄弟的本质区别
初学者最容易混淆的就是 XSS 的分类。记住一个判断标准就够了: 恶意脚本"留在哪里",决定了它是哪一型。
反射型(Reflected XSS,非持久型)。 你发送一个带有 payload 的链接给受害者,服务器把这个 payload 原样"反射" 回响应页面里,脚本只在这一次请求的响应中执行。恶意代码不留在服务器上。典型的入口是 URL 参数、搜索关键词。
存储型(Stored XSS,持久型)。 攻击者把 payload 提交到服务器(比如评论、留言、用户资料),服务器把它存进数据库。此后每一个访问该页面的用户,都会从服务器拿到这段被"储存"的恶意脚本并执行。危害最大,因为不需要诱导点击特定链接,访问即中招。
DOM 型(DOM-based XSS)。 服务器端的响应 HTML 可能完全没有问题,漏洞发生在浏览器端的 JavaScript 代码里:JS 从 URL、location.hash 等"源"中读取数据,然后把它写入 innerHTML、document.write 等"汇"中,浏览器在 DOM 层面把它解析成了可执行的代码。DOM 型 XSS 在 2005 年由 Amit Klein 正式定义和命名。
给小白的一句话总结:反射型是"服务器帮我递了一下刀",存储型是"服务器把刀存起来反复用",DOM 型是"服务器没碰刀,浏览器自己的 JS 把刀递了出去"。
二、环境搭建:用小皮面板把靶场跑起来
XSS-Labs 是一个 PHP 编写的 Web 应用,核心就是把源码放进 Web 根目录,让 Apache 能解析它。
步骤一:获取源码。 从 XSS-Labs 的 GitHub 仓库下载压缩包(搜索"xss-labs GitHub"即可找到多个镜像仓库,核心源码是 level1.php 到 level20.php 等文件)。
步骤二:放入 WWW 目录。 把小皮面板的网站根目录理解为"Apache 找文件的地方"。将解压后的 xss-labs 文件夹放到小皮面板的 WWW 目录下,例如 D:\phpstudy_pro\WWW\xss-labs\ 。
步骤三:创建站点。 在小皮面板中"创建网站",域名可以填 127.0.0.1 或你本机的局域网 IP,根目录指向刚才放入的 xss-labs 文件夹。PHP 版本选择 5.x 可以减少兼容性问题,因为靶场年代较早。端口如果 80 被占用,换一个偏一点的端口即可。
步骤四:验证。 浏览器访问 http://127.0.0.1/xss-labs/(如果改了端口,加上 :端口号)。看到关卡列表页,环境就就绪了。按 F12 打开浏览器开发者工具,后面的每一关你都会反复用到"元素(Elements)"面板------看清你的输入落在 HTML 的什么位置,是通关的唯一钥匙。

三、通关前的"心法":不要背 Payload,要看上下文
XSS-Labs 一共二十关左右,很多教程会让你背一堆 payload。背是背不完的,换一个过滤规则 payload 就失效了。真正需要养成的习惯是 四步分析法 :
第一步:找输入点。 URL 参数?表单?HTTP 请求头?还是 URL 的 hash 片段?
第二步:找输出点。 你输入的内容出现在响应 HTML 的哪个位置?是在标签之间(<div>你输入</div>),还是在属性值里(<input value="你输入">),还是落进了 <script> 代码块?
第三步:判断上下文。 这个位置的"边界字符"是什么?是双引号 "、单引号 '、尖括号 < >,还是什么都没有?你的第一任务永远是把当前上下文"闭合"掉,让自己获得"说话权"。
第四步:构造最小验证。 先不要上弹窗,先输入一个独一无二的字符串(比如 zztest123),看它原样出现在源码的哪里。位置看清了,payload 自然就出来了。
四、Level 1--10 逐关拆解
Level 1:没有任何过滤------你看到的第一个"裸奔"漏洞
原理与解释
页面把 name 参数的值原样输出在 <h2> 标签之间(<h2>欢迎用户 ...</h2>)。浏览器在这个位置期待的是子节点 :它可以是一个文本节点,也可以是一个新的元素节点。当输入以 < 开头时,HTML 解析器就把它当作标签开始,而不是文本。
通关步骤
1)URL 加 ?name=zztest123,F12 确认输入出现在 <h2> 内、未被转义。

2)构造 ?name=<script>alert(1)</script>。
3)<script> 开启脚本块,alert(1) 执行。

如何防御
输出到 HTML 标签之间时,必须做 HTML 实体编码 :< → <,> → >,& → &。PHP 中用 htmlspecialchars($input, ENT_QUOTES, 'UTF-8')。这是 OWASP"上下文输出编码"的最基本场景。
真实世界案例:早期 eBay 的反射型 XSS(1999 年前后)
OWASP Web Hacking Incidents Database 记录了一起"非常早期的 eBay XSS 问题",攻击方法为跨站脚本,目标是会话劫持 。值得注意的是,当时"XSS"这个术语甚至还没有被广泛使用。这与 Level 1 的结构完全一致:服务器把用户可控的输入未经编码放进 HTML,浏览器照单执行。一个"没有过滤"的标签间输出点,就是最干净的可利用入口。
这一关你学会了什么 :HTML 的"标签上下文" 。当你的输入位于两个 HTML 标签之间时,输入 <script> 就能开启一个新的脚本块。这是 XSS 最原始、最干净的形态,后面所有关卡都是在不同程度上阻止你"开启一个新的标签"。
Level 2:输入掉进了 value 属性里------闭合,是所有 XSS 的母题
原理与解释
输入被放进 <input value="你输入" ...> 的属性值 中。属性值内部是字符串上下文,浏览器不会把其中的 <script> 当作标签解析。所以直接注入 <script> 不生效------你必须先逃出属性值,回到标签之间的位置。
通关步骤
1)输入 zztest123,F12 查看源码,确认它在 value="..." 的双引号内部。

2)构造 "><script>alert(1)</script>。
3)第一个 " 闭合 value;> 闭合 <input> 标签;<script> 随后作为独立元素被解析执行。

如何防御
属性值必须用引号包裹,并对引号做 HTML 实体编码。htmlspecialchars 的 ENT_QUOTES 参数同时处理单双引号。但更重要的是:属性值里绝不能直接拼接未编码的用户数据,即使加了引号也不行------Level 2 证明引号本身可以被闭合。
真实世界案例:Wix.com 反射型 XSS(query 参数回显在标签属性中)
漏洞报告中描述的场景是:一个 query 参数被回显在 HTML 标签属性 里,形成反射型 XSS。这与 Level 2 的"输入落入 value 属性"属于同一类上下文错误。防御的关键不在于"有没有过滤 <script>",而在于属性值内部的数据有没有被正确编码,引号有没有被转义。
这一关你学会了什么 :"输出位置"的概念。 同一个 <script>,放在标签之间就执行,放在属性值里就不执行。XSS 的第一步不是写脚本,而是用边界字符把自己从"非代码区域"搬到"代码区域"。
Level 3:尖括号被转义了------当"标签"走不通,走"事件"
这一关输入 "><script>alert(1)</script>,查看源码会发现 < 变成了 <,> 变成了 >。

原理与解释
htmlspecialchars 把 < 和 > 转成了 < / >,标签注入这条路径被堵死。但这一关的 value 是用单引号 包裹的,而 PHP 早期常见的 htmlspecialchars($input) 默认不转义单引号 。同时,HTML 的事件属性 (onfocus、onclick、onmouseover)接受 JavaScript 代码,且不需要尖括号。
通关步骤
1)确认源码为 value='...'。

2)构造 ' onfocus=alert(1) autofocus='。
3)第一个 ' 闭合 value;onfocus=alert(1) 给 <input> 添加事件;autofocus 让页面加载时自动聚焦触发;末尾 ' 合法闭合剩余引号。
4)页面加载即弹窗。

如何防御
对属性值使用引号包裹 + 对引号做实体编码 。如果必须允许事件属性,事件处理代码应由服务端生成或严格限制,绝不允许用户输入直接成为属性名或属性值的一部分。正确的防御不是"过滤 on",而是确保用户数据永远无法逃出当前属性。
真实世界案例:Twitter 2010 年 "onmouseover" XSS 蠕虫
2010 年 9 月,Twitter 旧版 Web 界面被一个使用 onmouseover 命令的 XSS 蠕虫攻击。用户不需要点击 ,鼠标悬停在一条推文上就会触发脚本执行,自动发布恶意链接并自我传播。挪威开发者 Magnus Holm 编写的第一版蠕虫在短时间内传播到至少 20 万 用户;英国前首相夫人 Sarah Brown 的账号也被波及,链接被重定向到色情网站。Twitter 安全负责人承认,该缺陷一个月前已修补,但一次更新"在不知情的情况下让它重新出现"。这一事件的核心机制与 Level 3 完全同源:事件属性是 JavaScript 的执行入口,不需要 <script> 标签 。防御失败的点不在于"尖括号有没有过滤",而在于事件属性没有被禁止或没有被正确编码。
这一关你学会了什么 :XSS 不只有 <script> 标签。 任何能触发 JavaScript 的 HTML 属性,都是潜在的注入点。同时也理解了 htmlspecialchars() 默认只转义双引号、不转义单引号的问题------这是早期 PHP 开发中非常经典的防御不完整案例。
Level 4:换了一对引号,逻辑完全一样
这一关的 value 变成了双引号包裹。
原理与解释
与 Level 3 逻辑完全相同,只是 value 变成了双引号 包裹。单引号 payload 失效,因为闭合字符不匹配。这不是新知识,而是强迫你养成"先看源码、再写 payload"的习惯。
通关步骤
1)F12 确认 value="..."。

2)构造 " onfocus=alert(1) autofocus="。
3)闭合、事件、自动聚焦,触发。

如何防御
同 Level 3。引号类型不是安全边界,引号是否被编码才是。开发者不能假设"我用了双引号,用户就逃不出去"------Level 4 演示了双引号同样可以被闭合。
真实世界案例:ECShop 4.0 反射型 XSS 中的引号闭合
在 ECShop 4.0 的一个反射型 XSS 分析中,攻击者在 Referer 头中构造了带引号闭合的 payload,将 <a href=javascript:...> 注入到页面中。虽然该案例的输入源是 HTTP 头,但它演示的引号闭合逻辑 与 Level 4 一致:属性值用双引号包裹,攻击者用 " 闭合后插入新的标签结构。引号类型只决定 payload 长什么样,不决定能不能被绕过。
这一关你学会了什么 :引号类型的敏感性。 ' 和 " 在 HTML 中不是可以随意互换的,闭合必须"对号入座"。
Level 5:on 被过滤了,但 href 还在
输入 "><script>alert(1)</script> 或 onfocus,查看源码会发现 script 变成了 scr_ipt,onfocus 变成了 o_nfocus------服务器对 on 和 script 做了字符串替换。

原理与解释
服务器把 script 替换成 scr_ipt,把 on 替换成 o_n,导致标签和事件属性两条路同时被堵。但 <a> 标签的 href 属性 可以接受 javascript: 伪协议------点击链接时,浏览器把后面的内容当作 JavaScript 执行。href 本身不含 script,javascript: 也不含 on,因此穿过了字符串替换。
通关步骤
1)输入 "><a href="javascript:alert(1)">click</a>。
2)"> 闭合 input 的 value 和标签;<a> 插入新链接;href 使用 javascript:。
3)页面出现可点击链接,点击触发。


如何防御
对所有 URL 类型的属性(href、src、action 等)实施协议白名单 :只允许 http:、https:、相对路径等安全协议,明确拒绝 javascript:、data:(在需要时)、vbscript: 等。黑名单替换 script 和 on 是典型的"打补丁式防御",必然有遗漏。
真实世界案例:javascript: 伪协议绕过修复的多个 CVE
这类错误在真实框架中反复出现。CVE-2024-34343 涉及 Nuxt 框架的 navigateTo 函数:它试图阻止 javascript: 协议 ,但没有正确使用底层 API,导致协议仍然可以被绕过。同类地,@nuxtjs/mdc 的 Markdown 锚点解析中,围绕 javascript: 协议的防护被绕过,导致 XSS。这些案例与 Level 5 共享同一个教训:针对 javascript: 的"黑名单检查"如果实现不严谨,就等于没有 。正确的做法不是"检测并拒绝 javascript",而是只允许白名单内的协议。
这一关你学会了什么 :javascript: 伪协议。 它让 <a> 标签的 href 变成了一个 JavaScript 执行入口。这也是为什么很多防御不仅要过滤 script,还要过滤 javascript: 的原因。
Level 6:大小写绕过------过滤器的"视力"可能不好
服务器开始对 script、on、href 等关键词做替换,但它只匹配小写。
原理与解释
过滤函数只识别小写 的 script 和 on。但 HTML 标签名和属性名不区分大小写 :<SCRIPT> 和 <script> 对浏览器完全等价,而 str_replace('script', ...) 是大小写敏感的。
通关步骤
1)尝试 "><SCRIPT>alert(1)</SCRIPT>。

2)过滤函数找不到小写 script,原样放行。
3)浏览器把 SCRIPT 当作 <script> 执行。

如何防御
如果你使用黑名单(不推荐 ),必须在比较前对输入做规范化(小写化、去空字符等)。但更根本的教训是:黑名单的大小写敏感性本身就是可被系统性利用的缺陷。白名单 + 输出编码不需要"猜"攻击者会用大写还是小写。
真实世界案例:MySpace Samy Worm 中的大小写与关键词变形
Samy 蠕虫在绕过 MySpace 过滤时,面对 javascript 被过滤的情况,使用了 java<换行>script 的方式让关键词变形。MySpace 的过滤器没有对换行做规范化,也没有对关键词做完整的语法解析。这与 Level 6 的"大小写变形"属于同一类缺陷:基于字符串字面量的黑名单,对同一语义的变形形式是脆弱的 。Samy 蠕虫最终在 20 小时内感染了超过 100 万用户。
这一关你学会了什么:HTML 是大小写不敏感的,而粗糙的黑名单是大小写敏感的。 这是黑名单防御最经典的缺陷之一。从这里开始,你看到的不再是"有没有过滤",而是"过滤器的实现质量如何"。
Level 7:大小写也被堵了------双写绕过
原理与解释
服务器先 strtolower 再做关键词删除。但删除函数通常只执行一次 :如果输入是 sscriptcript,删除中间的 script 后,剩下的两段拼起来正好又变成 script。等价地,oonfocus 删掉一个 on 后剩下 onfocus。
通关步骤
1)构造 "><sscriptcript>alert(1)</sscriptcript>。

2)过滤执行一次,sscriptcript → script。
3)浏览器得到合法的 <script> 标签。

如何防御
防御不能依赖"删除一次关键词"。如果必须做关键词过滤,必须循环替换直到稳定 ,或者更好:不做关键词黑名单,改用解析器/白名单 。str_replace 的单次调用是典型的过滤实现缺陷,而不是"过滤思想"本身的安全。
真实世界案例:Yahoo! 邮件/Webmail 蠕虫的单次过滤缺陷
OWASP 文档在讨论黑名单过滤的不可靠性时,明确以 Yahoo! 的 HTML/JavaScript 过滤器为例:该过滤器只应用一次 ,因此带有蠕虫代码的 onload 属性在经过一次过滤后仍然留在原地。这与 Level 7 的"单次替换"缺陷完全对应:过滤只做一轮,就意味着变形输入只要设计成"过滤后恰好复原",就能穿过。同页还记录了另一个 Webmail 蠕虫 Nduja,针对四家意大利邮件服务进行跨域传播。
这一关你学会了什么 :"替换一次"不等于"替换干净"。 防御逻辑如果只做单次字符串替换,双写就能复活关键词。安全的过滤必须是循环替换或基于解析器,而不是简单的 str_replace 一次了事。
Level 8:关键词被彻底拉黑------HTML 实体编码绕过
原理与解释
过滤对 script、on、src、data、href、双引号 " 做了严格处理:不再只是简单黑名单匹配,匹配到关键词就插入下划线破坏,同时输入里的 " 会被转义为 ",无法用引号闭合标签、不能自己新建标签;大写、双写在这里也全部失效。
页面原生自带 <a href="用户输入">友情链接</a>,我们的输入只会填充到这个 href 属性内部。 可以利用 HTML 字符实体:&#十进制数字;、&#x十六进制;。服务器拿到的原始字符串是一堆实体编码,不存在连续的 script、href 等关键词,不会被加下划线破坏;浏览器解析 href 属性时,会先对 HTML 实体完整解码,还原出 javascript:alert(1),再识别伪协议执行 JS。
重点误区:不要自己写
"><a href="xxx">,这是典型错误写法。本关卡双引号被转义,这条 payload 会直接被破坏,最终出现hr_ef带下划线的结果,无法触发 XSS。
通关步骤
1)构造完整实体编码的伪协议,不添加引号、不构造标签闭合、不新增 a 标签:
javascript:alert(1)
十六进制版本备选:
javascript:alert(1)

2)提交输入,服务器扫描字符串,找不到连续敏感关键词,不会插入下划线,直接放行填充到原有 <a> 的 href 属性。
3)浏览器渲染页面,自动解码全部 HTML 实体,还原得到 javascript:alert(1);点击页面上的【友情链接】,触发弹窗,通关。

如何防御
这一关揭示了编码层级问题:服务器字符串检测和浏览器解析是两个不同阶段,同一段数据在两个阶段看到的形态完全不一样。 单纯黑名单、关键词替换几乎防不住各类编码绕过。防御不能只在输入阶段过滤关键词。
-
如果内容要放进 HTML 属性:输出阶段做规范的 HTML 属性编码;
-
如果是链接、href 这类 URL 属性:优先使用协议白名单 (只允许
http://、https://); -
校验前必须先按照浏览器的解码规则做规范化解析,而不是直接对原始字符串做简单匹配,否则实体编码后的伪协议会绕过检测。
真实世界案例:Joplin Markdown 渲染中的 javascript: 图片地图攻击
在 Joplin(Electron 桌面应用)的漏洞中,Markdown 渲染支持 <map> / <area> 标签,href 属性允许 javascript: 伪协议,并且可以借助 Electron 的能力实现沙箱逃逸、执行任意系统命令。 漏洞本质和 Level 8 一致:只做了粗糙的关键词过滤,没有对 URL 属性做协议白名单校验,攻击者可以依靠各类编码变形绕过字符串检测,最终浏览器解析还原伪协议。问题根源不是没删掉 javascript,而是没有在 URL 上下文里做可靠的协议限制。
这一关你学会了什么:编码层级
同一份数据,在「服务器原始字符串检测」和「浏览器解析渲染」两个阶段呈现形态不同。HTML 实体编码、URL 编码、Unicode 转义、十六进制编码,都是突破静态黑名单的常用手段。 防御核心:在正确的上下文做对应的编码与校验,不要依靠黑名单猜测、拦截关键词。
Level 9:必须包含 http://------ 编码 + 注释骗过存在性检测
原理与解释
本关继承 Level8 的全套过滤规则:小写转换、匹配 script/on/src/data/href 就插入下划线破坏关键词、双引号 " 转为 ",不能闭合标签,输入只能塞进页面原生 <a href="输入">友情链接</a> 。额外新增一条校验:后端用 strpos 判断处理后的字符串里是否存在子串 http://;找不到就直接替换链接文本,不会把你的输入放进 href。
直接写 javascript:alert(1)/*http://*/ 会失败:原始字符串里有连续 script,后端替换成 scr_ipt,JS 伪协议直接废掉。 正确思路是组合两层技巧:先用 HTML 实体编码把 javascript: 打散,让后端扫描不到连续 script,不会加下划线;再在 payload 末尾带上 http://,并用 JS 注释包裹它。后端在原始字符串中看到了 http://,放行;浏览器解析 href、解码实体之后,再执行 JS,注释会吞掉这串 http://,不会干扰弹窗逻辑。
重点误区:只靠注释、不对
javascript编码是经典错误写法,会触发 script 关键词替换,payload 直接失效。
通关步骤
1)构造完整 payload(整条 javascript:alert(1) 实体编码,末尾追加注释包裹的 http://),不要引号、不要自己写 a 标签: 十进制版本(推荐)
javascript:alert(1)/*http://*/
十六进制备选:
javascript:alert(1)/*http://*/
单行注释版本同样可用:
javascript:alert(1)//http://

2)提交输入,后端扫描原始字符串:找不到连续的 script,不会插入下划线;同时成功匹配到 http://,通过 strpos 检测,将编码串填入 href 属性。
3)浏览器渲染页面,先解码全部 HTML 实体还原出 javascript:alert(1);JS 引擎执行时,/*http://*/ 被识别为多行注释直接忽略;点击友情链接,弹窗触发,通关。

如何防御
不要用 "原始字符串是否包含某个子串" 作为 URL 合法性判断。 如果业务需要接收外部链接:
-
优先做协议白名单,只放行
http:、https:; -
校验前,按浏览器解析规则做完整解码、规范化,再提取协议字段;
-
不能只在原始文本里搜索
http://,攻击者可以把校验串塞到 JS 注释、字符串、URL fragment 中,骗过简单存在性检测; -
对 href、src 这类 URL 属性,统一使用成熟 URL 解析库,而不是手写字符串匹配。
真实世界案例:协议校验被注释 / 填充绕过的 CVE 模式
CVE-2024-34343(Nuxt)的 navigateTo 防护试图拦截 javascript: 伪协议,但校验逻辑停留在简单字符串匹配;@nuxtjs/mdc 同类防护也出现过绕过。这类漏洞共性:安全校验只对原始输入做子串查找,没有在浏览器的解析语义层面解析 URL。攻击者通过编码变形、JS 注释、填充无关文本,制造 "检测阶段合法、执行阶段恶意" 的错位,和 Level9 的绕过逻辑完全一致。本关是这类漏洞极简教学模型。
这一关你学会了什么:存在性检测很容易被语义错位欺骗
过滤器要求 "输入必须包含 X",不等于 X 会被浏览器当成 URL 的一部分执行。 同时你要记住:多道过滤是叠加生效的,上一关的编码要求在本关依然强制生效,注释只是用来应付新增的 http:// 检查,不能省略实体编码。字符串黑名单 + 简单子串校验的组合防护,很容易被「编码打散关键词 + 填充校验诱饵 + JS 注释」这套组合拳击穿。
Level 10:隐藏的输入框 ------ 改写属性绕过尖括号过滤
原理与解释
本关不再过滤关键词,而是直接把输入里的 <、> 全部清空,所以我们不能新增任何标签;同时 keyword 参数使用 htmlspecialchars 严格转义,没有注入点。 页面原生表单内置了三个隐藏 input:t_link、t_history、t_sort,只有 t_sort 的值直接拼接进 value,没有做 HTML 转义。 原始标签是:
<input name="t_sort" value="用户输入" type="hidden">
我们用 " 闭合 value,然后追加新属性:覆盖掉原来的 type="hidden",改成 type="text",把隐藏框暴露在页面上,再挂载事件处理器执行 JS。
经典坑:
onfocus + autofocus + alert会无限弹窗。alert 是阻塞对话框,点击确定后焦点会返还给输入框,再次触发 onfocus,形成死循环。
通关步骤
1)构造 payload,闭合 value、重写 type、挂载点击事件:
" onclick=alert(1) type="text"
完整 URL:
level10.php?t_sort=" onclick=alert(1) type="text"
2)后端清除 < >,其余字符原样回显,最终页面渲染结果:
<input name="t_sort" value="" onclick=alert(1) type="text" type="hidden">
后面的 type="hidden" 会被前面先出现的 type="text" 覆盖,输入框显示出来。

3)页面出现文本框,点击它,触发 onclick,弹窗执行,一次通关,不会循环弹窗。

备选(自动触发版,加 blur 防循环,适合理解 autofocus)
" onfocus="alert(1);this.blur()" type="text" autofocus="
如何防御
这一关暴露一类很常见的漏洞:直接把用户可控数据拼接进标签属性值。
-
所有输出到 HTML 属性的值,统一做 HTML 实体编码;
-
不要只过滤尖括号,攻击者不需要新增标签,只需要追加、覆盖已有属性;
-
黑名单过滤标签符号非常脆弱,攻击者可以篡改原有标签的行为;
-
如果业务有多个同名字段、动态属性,注意:浏览器会优先采用前面的属性值,后面同名属性会被忽略。
真实世界案例:属性注入(Attribute Injection)
很多 CMS、后台统计页面、日志页面会把 sort、order、referer、ua 这类参数直接塞进隐藏表单。只简单过滤尖括号,忽略属性注入,导致攻击者追加 onclick / onmouseover / onfocus 事件。 这类漏洞经常被低估:看起来 "不能插新标签很安全",但篡改已有元素的事件处理器,效果和普通 XSS 完全一致。典型场景:后台列表排序参数、埋点统计隐藏字段。
这一关你学会了什么:属性注入
XSS 不一定非要写 <script>,也不一定非要新建标签。 在已有标签的属性值可控时,可以用引号闭合,追加、覆盖属性,挂载事件。 同时要理解浏览器事件和焦点的交互:alert 这类阻塞弹窗会改变焦点时序,很容易制造 "无限弹窗" 这种反直觉现象;写 payload 时要区分:自动焦点、鼠标悬浮、点击事件各自的副作用。
五、总结
整套 XSS-Labs Level1--10 的闯关,本质就是沿着 HTML 的解析规则,一步步击穿粗糙黑名单防御的过程:从最基础的标签注入、属性引号闭合,到事件绕过、伪协议、大小写、双写、实体编码、注释诱饵、属性覆盖,所有 payload 都不是凭空猜出来的。它反复告诉我们一条核心准则:没有万能的关键词过滤,XSS 的胜负取决于数据落在什么上下文,防御必须匹配上下文做输出编码、协议白名单与语义解析,而不是依靠简单字符串匹配去拦恶意字符。
**重要声明:**本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。