恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
XSS绕过方法核心逻辑与防御:从编码到上下文逃逸
首页
资讯中心
/
XSS绕过方法核心逻辑与防御:从编码到上下文逃逸
XSS绕过方法核心逻辑与防御:从编码到上下文逃逸
发布时间:2026/10/13 7:31:06
做Web安全的人基本都会经历一个阶段觉得XSS很简单不就是script、onerror、javascript:那几招再往后又会发现好像随便写点什么东西都能被过滤掉于是开始研究XSS攻击绕过方法。我当年也是这样从一个只会背payload的小白到能在授权项目里快速判断一个过滤规则该怎么破花了不少时间踩坑。这篇东西我想把绕过方法的核心逻辑、分类思路和防御对策一次性讲透标题虽说是100种绕过方法但比起硬凑100个变体我更想帮你把为什么能绕这件事理解清楚。写这些内容之前先声明一下所有讨论都限定在你有明确授权的测试项目、自建靶场、CTF题目或开源漏洞环境里进行。绕过技术本身是中性的它最大的价值不是用来打谁而是让你知道自己的过滤规则到底哪里会漏。下面进入正题。1. 先搞清楚一件事绕过研究到底在研究什么1.1 授权边界与实验环境很多人一听到绕过两个字就自动脑补成攻击者的游戏。实际上我在日常工作中接触到的绕过研究绝大多数是防御侧的自我检验上线前做代码审计、安全测试、WAF规则调优、漏洞复测都需要模拟攻击者的思路。没有这个前提任何绕过技巧都只是在给自己找麻烦。所以第一步永远是确认环境。我常用的实验组合是本地搭一个带过滤逻辑的PHP或Node测试页前端放一个输入框后端写几行简单的黑名单正则然后反复输入各种变形payload观察浏览器实际执行情况。也可以用现成的开源靶场或者在专业测试平台上做验证。重点在于你每次触发执行都能看到完整的输入-过滤-输出链路这样你才知道是哪一步放行了。1.2 两类过滤器的判断逻辑要把绕过研究明白得先知道你面对的是什么类型的过滤器。黑名单过滤是最常见的也是绕过研究的重灾区。它的逻辑是我知道哪些东西危险把它们拦掉。问题在于黑名单永远是不完整的你拦了script我可以拆开你拦了onerror我可以换onload、onfocus、onmouseover你拦了alert(1)我可以用confirm、prompt、eval、Function构造函数。黑名单的每一项都需要人工维护每天都在跟攻击者的创造力赛跑。白名单过滤则是我只允许特定字符或特定模式出现比如只允许字母数字、只允许特定的标签和属性。这类过滤相对安全但也不是无懈可击如果白名单校验只作用于参数值而忽略了对输出位置的判断依然可能在上下文逃逸处出问题。另一类是框架内置编码比如模板引擎自动对输出做HTML实体转义看起来安全但如果你用了v-html、innerHTML、dangerouslySetInnerHTML这类跳过编码的API框架的保护就等于被手动关掉了。1.3 100种方法背后的方法簇市面上各种XSS绕过大全列了一百多个payload看起来眼花缭乱其实如果你按底层原理拆开会发现数量并不多。我自己的分类习惯是这样编码类利用不同编码体系之间的转换差异让过滤器认不出、浏览器却认得。上下文逃逸类在属性、事件、脚本、样式等不同上下文里通过闭合、截断、重组来进入可执行区域。解析器怪癖类利用HTML解析器的容错机制、大小写、空白字符、重复属性、不完整标签等特性。协议与数据流类在WAF或代理层利用请求格式、Content-Type、字符集等差异绕过检测。数据流污染类不是从字符串层面绕过而是利用DOM Clobbering、Mutation XSS这类运行时才暴露的漏洞。我后面几节会逐个展开并在第6节给一个速查表。你只要掌握了这些方法簇面对任意一个具体过滤器时都能现场组合出几十种变体100种只是这个组合空间的下限。2. 编码类绕过让过滤器认不出的常见手法2.1 HTML实体编码数字实体与命名实体HTML实体编码是最容易理解的绕过思路。当用户输入被插入到HTML文档中浏览器在解析HTML标签时会先做实体解码再判断标签结构。但很多过滤规则是在解码之前运行的它只看到一串#x3c;script#x3e;以为没有尖括号就放行了。最常见的实体写法有三种命名实体、十进制数字实体、十六进制数字实体。比如可以写成lt;、#60;、#x3c;。如果过滤规则只拦了script这种完整形态那#x3c;script#x3e;直接就能跑起来。我遇到过一个简单的Python过滤脚本只做了字符串替换把替换成lt;结果又被它自己替换出来的lt;骗了——因为输出端没有再做一次实体编码导致双重编码后浏览器依然能识别。但也要说明一个前提HTML实体编码的绕过只在HTML上下文标签、属性值最外层有效。如果输入是放在script标签内部的JavaScript代码里HTML实体解码不会发生在JS解析阶段很多实体写法反而会失效。这也是为什么我总跟团队强调别只看一个payload能不能弹窗要看它所在的上下文。2.2 URL编码与双重编码的条件URL编码在绕过中的应用场景主要是href、src、action这类带URL语义的属性。过滤规则往往会对javascript:、data:、http:等协议关键字做黑名单但你输入javascript%3A时WAF可能不认识这个变形而浏览器在解析URL属性时会自动做一次URL解码。我第一次在授权项目里遇到这个场景是在一个图片链接参数上。服务端把javascript:关键词直接过滤为空我改成java%73cript:后端没拦住浏览器解析URL时先把%73还原成s再跳转到伪协议执行。这就是典型的过滤器只检查原文、浏览器却会解码的错位。双重编码的逻辑类似输入%253Cscript%253E如果你的服务端对参数做了一次URL解码然后输出到页面上WAF看到的是%3C可能放行而浏览器端如果又根据页面编码解析一次就能还原成script。这种方法能否成功取决于服务端到底做几次解码、输出编码怎么处理所以每次都要实际测试不能凭感觉。2.3 JavaScript上下文里的Unicode与模板字符串在script上下文里过滤器的常规姿势是拦alert、eval这类函数名。绕过思路通常不是靠HTML实体而是靠JavaScript自身的解析特性。一个经典手法是利用Unicode转义序列。JavaScript字符串中\u0061lert在运行时会被解析成alert如果过滤器只搜原始文本里的alert它看不到这个转义形态就能放行。再比如eval可以用Function构造函数代替alert(1)可以改成(1, alert)(1)、[].constructor.constructor(alert(1))()这种利用构造函数链的写法。过滤规则越复杂这种等价写法的空间反而越大。模板字符串也是个常用技巧。反引号允许你插入表达式alert可以不作为字符串出现而是通过拼接生成比如top[alert](1)。关键点在于JavaScript的灵活性远高于任何黑名单的覆盖能力只要执行环境允许总有办法找到未被枚举的函数或调用方式。2.4 编码叠加为什么会奏效单个编码可能被拦多组编码叠加就基本靠解析顺序决定成败。我曾经调试过一个案例输入从URL参数进入后端做了一次urllib.unquote然后拼进HTML属性前端又用JavaScript读取该属性值赋给location。三次解析里只要有一层浏览器认为是合法数据另一层过滤器认为是安全数据绕过就成立了。在实际测试时我建议把这个过程拆成几步记录输入原始形态是什么服务端做了什么解码输出在哪个HTML上下文最终由哪个解析器消费每一步都写清楚之后你就能准确判断该用哪种编码组合。这也是为什么我鼓励团队成员把编码叠加当成一道解析链题目来做而不是碰运气。3. 上下文逃逸类从属性、事件到协议3.1 属性值内的闭合与注入如果说编码绕过是让过滤器认不出那上下文逃逸就是让过滤器以为它管住了其实你跑出去了。最常见的场景是用户输入被拼进input value...这样的属性里。此时过滤器如果只检查了script、onerror这些词你只需要用引号闭合属性再制造一个新属性或标签即可。比如输入svg/onloadalert(document.domain)前面部分闭合了value属性和input标签后面就变成了一个全新的SVG标签。我见过不少新手在这里踩坑他们以为有过滤就能防住但过滤器根本没检查和的上下文关系。这里有一个容易被忽略的细节属性值里如果用了单引号后端只转义了双引号那单引号闭合照样有效如果输入被用htmlspecialchars转义了、、、引号那直接闭合是走不通的这时候要考虑是否还有实体解码导致的双重编码问题或者该属性本身的语义可以被操纵比如style、href。3.2 事件处理器与伪协议事件处理器是属性注入里的快速通道。onerror、onload、onfocus、onmouseover、onclick每一个都能在特定条件下触发脚本。过滤器通常会把常见的on事件列进黑名单但它很难穷举所有事件像onpointerenter、onwheel、ontoggle、onanimationend这种冷门事件就经常成为漏网之鱼。伪协议则是另一类执行入口。javascript:可以出现在a href、iframe src、form action等位置data:协议在部分浏览器中配合text/html也能执行脚本。测试策略很简单列出所有带URL语义的标签属性然后用最简单的alert(1)去试看哪个位置没有被过滤覆盖。需要特别留意的是有些过滤规则会把javascript:中的冒号去掉或者把script关键字去掉但如果你用回车符、Tab、注释符、实体编码把关键字拆开或者利用HTML解析器对空白字符的容忍过滤规则就很容易失效。比如java\nscript:在URL解析中可能不合法但某些浏览器对URL内部的控制字符处理比较宽松对于一些旧的解析流程会出现意外执行的案例。3.3 标签拆分、关键字重组与注释干扰当输入被拼在HTML标签名或属性名的位置时过滤器如果直接把script替换为空你就可以用嵌套拆字来绕过scrscriptiptalert(1)/scr/scriptipt。第一次过滤去掉内层的script后剩下的反而拼成了完整的script标签。这种过滤方式之所以可笑是因为它用字符串替换去处理一个结构化语法必然会产生重组机会。注释干扰也很常见。HTML注释!-- --和CSS/JS里的注释块都能打断过滤器的字符串匹配但到了浏览器解析阶段注释会被忽略剩下内容依然能拼成可执行结构。比如在scr!--x--ipt这种形态里注释会打断过滤器看到的连续script字符串但浏览器解析HTML时不会把它当作有效注释因为注释只允许出现在特定位置实际效果反而是标签名被拆开却不影响闭合。这种情况要逐个验证不能光靠理论。3.4 CSS、SVG与MathML里的脚本执行点非主流上下文经常被人忽略但它们是绕过过滤器的高产区域。CSS里古老的expression()虽然现代浏览器已经不执行但在兼容模式或某些特定渲染条件下仍是历史遗留问题import可以加载外部样式表配合-moz-binding这类老特性也曾在特定版本浏览器里触发脚本。SVG是现在更实用的方向。SVG属于XML系但内嵌在HTML中时解析规则很灵活svgscript、svg/onload都是常见执行点。MathML同理它把命名空间和HTML混在一起一些过滤器只针对HTML标签做了白名单看到math就直接放行结果里面藏着事件处理器。测试时我的习惯是除了div、span、input这类常规标签专门把svg、math、details、marquee、video、audio这些非典型脚本容器过一遍。4. 解析器的宽容与浏览器怪癖4.1 大小写、空白字符与格式化字符大量过滤规则用正则匹配时没加i标志或者只匹配小写关键字。HTML标签和属性名是大小写不敏感的所以SCRIPT、ScRiPt在执行效果上和script完全一样很多过滤器却只会拦小写那一版。我第一次在别人代码里看到这种漏洞时就意识到正则的健壮性永远不能假设攻击者会按你期望的格式输入。空白字符是另一个被低估的点。HTML解析器允许标签名和属性名之间有Tab、换行、回车、空格等字符甚至在某些位置允许\0空字节被忽略。过滤器如果用严格的字符串匹配去搜onload那onload 就可能绕过。当然现代浏览器对无效字符的处理越来越严格但这份宽容在属性名的某些位置依然存在尤其在拼接HTML片段时。4.2 重复属性与参数污染HTML解析器遇到重复属性时浏览器普遍会取第一个或最后一个不同浏览器之间还有差异。过滤规则如果只是简单地获取属性值然后检查它可能取了第一个而浏览器执行时用的却是第二个——这种错位就能用来绕过。我曾经在某个项目里看到过滤逻辑是先把所有on*属性值里的内容清空。攻击者构造img onerroralert(1) onerrorx如果浏览器取第一个任务会被第一个拦截但如果过滤器按顺序迭代删除了所有onerror而浏览器恰好取的是最后一个被删掉的属性那还是能拦下来。反过来如果浏览器取第一个属性过滤器却只检查了最后一个也一样会出现绕过。这块没有万能的结论只能实测你目标浏览器的行为。参数污染HPP在URL层面也有类似效果。同一个参数名出现多次时不同语言和框架会取第一个、最后一个或者拼接全部。WAF可能只检查了其中一个值而应用层实际用的是另一个值绕过就发生在框架与WAF对参数处理规则的差异上。4.3 不完整标签带来的注入机会HTML解析器的容错机制很出名这也给XSS绕过提供了特殊土壤。比如img srcx onerroralert(1)少写一个浏览器照样会正确解析并把后面的内容当作当前标签的延续。这意味着即使过滤规则要求用户输入里不能出现比如把转成gt;只要你能在前面构造出一个未闭合的标签后面的载荷一样能生效。svg/onloadalert(1)这种不带闭合符的写法也很常用因为SVG内部解析规则允许。再比如body/onloadalert(1)在属性值之前用/代替空格也是利用了解析器的宽容。做这类测试时我建议你打开浏览器开发者工具直接看最终的DOM树结构别只盯着响应源码——因为源码和渲染后的DOM经常不一样。4.4 Mutation XSS与DOM Clobbering这两类问题不在字符串层面解决而是在DOM操作运行时才出现。Mutation XSS指的是你用innerHTML插入一段看似安全的HTML但浏览器在解析和修正DOM时会把某些内容变异成可执行脚本。最典型的场景是把noscript、template、textarea、title包裹的内容来回移动逃离了原本安全的上下文。DOM Clobbering则是靠HTML元素的id或name属性污染JavaScript的全局对象。比如页面里有一个form idx然后window.x就被指向了这个表单如果再构造img idx配合x.alert这种访问链某些较老的应用代码会被污染后的对象骗过检查进而改变程序流程。这类问题很难靠过滤规则解决因为它利用的是HTML和JavaScript共享命名空间这个基础机制。5. WAF与框架层面的对抗协议与数据流的角度5.1 Content-Type与载荷传输方式很多过滤规则把精力集中在参数值的字符串匹配上却忘了同一份载荷可以通过不同的请求格式送进来。最典型的是把application/x-www-form-urlencoded改成multipart/form-data或者反过来WAF对两种格式的解析深度不一致就会有一边漏检。我在调WAF规则时发现不少产品对JSON格式请求体的解析很浅尤其是嵌套层级一深检测引擎只检查了第一层。攻击者把payload塞进第三层或第五层就能轻松绕过。这背后不是WAF厂商不用心而是协议解析的完备性本身就是工程难题你永远不知道后端框架会怎么处理一个畸形Content-Type。5.2 字符集声明与编码识别差异字符集是另一个容易造成所见非所得的点。HTTP响应头里的Content-Type如果没指定charset浏览器会根据内容去猜编码。WAF是按UTF-8解析的页面实际用的是GBK那一些在GBK里能被解析为特殊字节序列的输入WAF可能完全看不懂。这种编码错位利用起来比较考验经验。一个常被提到的现象是某些中文字符的字节序列里会包含0x3c等字节在GBK解码时它属于某个汉字的一部分过滤器因此放行但浏览器在某些解析路径下会把那个字节重新识别为。这种情况我见过不止一次每次排查都挺费劲。防御端最稳妥的解法是统一声明UTF-8并且在后端对输入做严格的字节级校验。5.3 数据规范化为什么WAF漏判WAF的检测对象是请求而应用的执行对象是解码后的数据。如果WAF没有和应用执行完全相同的解码顺序绕过就几乎是必然的。比如WAF看原始URL编码的payload应用做了解码后再拼接或者WAF做了解码检查应用却直接把原始值拼进了某个会二次解码的位置。这里有一个很实用的测试思路不要只测一种编码形态而是把原始输入、一次解码、两次解码、实体解码、Unicode规范化全跑一遍。每跑一次记录过滤器和应用层各自看到的内容你就能画出一张解析链路差异表。绕过点往往就在这张表的不相同那一栏里。5.4 数据流绕过的典型形态框架层面的绕过往往不像字符串替换那么直观。比如模板引擎默认开启了自动转义但如果开发者用|safe、{!这种标记关闭了某段输出的转义过滤等于被人工绕过。再比如一个API接口先返回JSON前端拿到后用innerHTML渲染服务端再怎么过滤只要前端渲染时跳过了编码XSS就还存在。这些数据流绕过提醒我XSS从来不只是后端过滤或前端编码单点的问题而是整条链路的问题。你在测试时一定要跟着数据流走从输入点、处理点、输出点一路看下去任何一个环节用了信任数据的姿势都可能成为绕过入口。6. 方法清单的归类速查把绕过能力装进工具箱6.1 按上下文分类的绕过速查表这里我把上面讨论的方法整理成一张可以反复查阅的速查表。注意这只是一个起点真正测试时要根据你的过滤规则、浏览器版本、输出位置做变体组合。上下文典型绕过思路示例形式HTML标签内容实体编码、标签拆分、注释干扰#x3c;script#x3e;、scrscriptiptHTML属性值引号闭合、事件属性、URL伪协议svg/onloadalert(1)、javascript:alert(1)JavaScript代码Unicode转义、构造函数、模板拼接\u0061lert(1)、[].constructor.constructor(alert(1))()CSSexpression、import、behavior老版本IE的expression(alert(1))嵌套命名空间SVG、MathMLsvg/onloadalert(1)、mathmtext/mtextscriptURL参数二次编码、参数污染、重复属性%25253Cscript%25253E、同名参数多次运行时DOMMutation XSS、DOM Clobberingnoscriptp title/noscriptimg srcx onerroralert(1)这张表覆盖了我日常测试里90%以上的场景。真正到了实战我会在每个上下文里再衍生出若干变体字段拆分、大小写、换行、空字符、全角字符、Base64编码、JSFuck化等等。100种说法一点都不夸张关键是你要知道每个变体是在变什么。6.2 按过滤器类型分类的优先级面对黑名单优先试大小写、编码、拆分重组、等价函数替换面对白名单优先看它是否只校验了参数值而没管输出位置以及是否有危险API被使用面对WAF优先看Content-Type、字符集、请求体解析深度和重复参数处理面对框架自动转义优先找那些被标记为安全的渲染API。我把这个优先级写在测试笔记的首页每次拿到一个测试目标先判断它属于哪一类再决定从哪个方向切入。盲目把payload列表从头打到尾效率低不说还容易漏掉真正有用的方法。6.3 如何建立自己的绕过测试笔记我自己的做法是维护一个Markdown笔记每个方法记录四件事输入样例、过滤规则原文、输出上下文、浏览器行为。刚开始记录时内容很乱后来整理成统一的模板查找就方便多了。比如这样一条场景PHP反引号替换输入scrscriptiptalert(1)/scr/scriptipt结果替换后重组为script执行成功这类笔记的价值在几个月后会显现出来。当你在另一个项目里遇到相似的过滤逻辑翻笔记比回忆快得多而且能帮你避免重复踩坑。我强烈建议新人从第一个测试项目开始就做这个记录越早越好。7. 把这些方法反转为防御手段7.1 输出编码的上下文感知编码方案既然绕过的根源是过滤器/编码器与应用解析上下文不一致那防御端最核心的动作就是做上下文感知的输出编码。HTML标签内容使用HTML实体编码HTML属性值使用属性值编码同样转义引号JavaScript字符串里做JavaScript编码URL属性里做URL编码。每个上下文用对应的一套编码规则不要图省事用一种编码套全部。实践中我见过很多代码把用户输入放进script变量里却只做了HTML实体编码结果引号、反引号、/script等字符原样进入JS上下文直接被打穿。正确的做法是避免把用户数据直接拼进JS代码必须拼时至少要做JSON序列化并对、、做转义以截断/script。7.2 CSP与SRI当作兜底再全面的输出编码也有漏网的时候所以CSP不能省。一个合理配置的CSP比如default-src self; script-src self即使攻击者成功注入了script标签浏览器也会拒绝加载内联脚本。当然CSP本身也有绕过话题所以建议配合nonce或hash机制管理内联脚本不要直接用unsafe-inline来省事。SRI子资源完整性主要针对外部脚本给script加上integrity属性能防止CDN或第三方脚本被篡改后执行。它和CSP一起构成了即使HTML被注入也不一定执行的纵深防御。7.3 过滤规则设计的常见错误我复盘自己踩过的坑最典型的就是用字符串替换做过滤把script替换为空、把alert替换为空。此类规则的漏洞在于替换后的字符串可能形成新的合法结构。真要写过滤规则至少要知道自己面对的解析器是什么做语法层面的校验而不是简单的关键词删除。另一个常见错误是把和全部转义当成万能方案。它在HTML文本上下文有效但在属性、URL、JavaScript上下文里转义和根本拦不住 onerror这类攻击。所以别再迷信过滤尖括号就能防XSS这句话真正要防的是在错误的上下文中使用可用数据。7.4 自我测试的闭环防御者的自我测试本质上是把第2到第5节的绕过方法全部变成回归用例。每写一段新的渲染代码、每上一条新的WAF规则都用这些用例跑一遍。我通常会把常见payload做成自动化脚本每次部署前执行一轮至少能挡住自己已知的绕过路径。同时要注意自动化脚本永远不能替代人工分析。绕过方法的组合空间极大自动化能覆盖的只是已知的已知。真正有价值的是你在测试新功能时顺手做一次上下文审计看一眼数据流经过哪些API、哪里会被浏览器二次解码。这套闭环坚持下来你会发现过滤规则和修复方案会越来越稳。最后说一点实际体会做XSS绕过研究最重要的不是记住多少payload而是建立一套上下文敏感的思维方式。每看到一个输入点先问自己三个问题——它会被哪几个解析器处理每个解析器的处理顺序是什么哪一个解析器和我写的过滤逻辑存在认知差异把这三个问题想清楚多数绕过路径你自己就能推演出来甚至不用翻任何速查表。我每次带新人都会逼他们亲手搭建一套含过滤的靶场环境把常见绕过方法从看图识字变成肌肉记忆这个投入非常值得。希望这篇整理也能帮你在绕过的迷宫里找到自己的地图。