XSS过滤绕过与WAF绕过:从标签原理到纵深防御体系
XSS为什么总是绕得过过滤器聊聊标签绕过与WAF绕过背后的攻防逻辑很多人跟我聊XSS过滤绕过时第一反应都是“给我几个payload我直接打”。但真正干过几年安全的人会明白绕过这个事本质上比的是“你比过滤器多知道多少种浏览器的解析规则”。标签绕过也好WAF绕过也罢背后拼的是对HTML解析、JavaScript执行环境、编码处理这几层机制的理解深度而不是死记硬背几个payload。这篇文章我想换个角度站在防御者和安全建设者的视角把这些绕过手法的原理拆开讲清楚——你看懂了攻击者为什么能绕自然就知道该在哪里堵。这篇文章适合三类人一类是做Web研发想知道为什么自己写了个过滤函数还是被打的一类是刚入门的安全工程师想系统理解XSS、过滤器、WAF这三者之间的博弈关系还有一类是做安全平台运营被WAF误报和漏报折腾得焦头烂额的同学。我不会给你一堆“复制即用”的攻击代码而是把思路、原理、防护策略和我在实战里踩过的坑讲透。读完之后你至少能回答这几个问题过滤标签为什么总有漏网之鱼WAF规则的盲区通常在哪儿一套靠谱的XSS防御到底该怎么搭1. 从一次“漏网”的弹窗说起过滤与绕过的底层逻辑1.1 先搞清楚攻击者和过滤器之间的博弈关系XSS攻击的本质是攻击者输入的数据被浏览器当成了代码来执行。那么防御的本质就是一句话让用户输入永远只能当数据不能被解析成HTML标签、JavaScript代码或者CSS表达式。听起来很简单但实现起来极其痛苦因为浏览器解析HTML的规则太复杂了。过滤器是大多数研发团队的第一道防线。常见的做法是黑名单把script、onerror、javascript:、alert(1)这些关键词给滤掉。但这个方案天然存在一个致命弱点——黑名单是有限集合而浏览器的解析规则是无限复杂的。攻击者不需要“正面突破”你的黑名单他只需要找到一个你没想到的编码方式、一个冷门的HTML实体、一个浏览器特有的解析差异就能让恶意代码绕过关键词匹配。我举个例子很多初学安全的人会觉得我把script标签禁掉、把onerror事件禁掉、把括号禁掉总该安全了吧结果攻击者用了一个svga xlink:hrefjavascriptcolon;alert(1)就绕过去了因为这里用了HTML实体编码colon;来表示冒号黑名单如果只查javascript:这个原始字符串是查不到的。这就是“过滤绕过”最基本也是最常见的形态不是你的黑名单不够长而是你没有跟上浏览器的解析规则。1.2 从“标签绕过”理解为什么标签黑洞永远堵不完“标签绕过”这个词我在安全社区里看大家讨论得很多。所谓的标签绕过就是攻击者不使用script这种众所周知的标签而是找那些同样能执行JavaScript但不在你黑名单里的标签。HTML里有上百种标签其中能触发脚本执行的有多少种我随手就能举出一串img、svg、body、video、audio、iframe、math、details、marquee……每一个都支持事件属性比如onerror、onload、onfocus每一个都可能被用来执行脚本。更麻烦的是同一个标签在不同浏览器里的解析行为还不一样。比如details标签配合open属性和ontoggle事件在Chrome里能弹窗在某个老版本的浏览器里可能不执行。所以那些“通杀payload”往往是一个很长的列表里面每个payload针对的是不同的标签和不同的执行链。你要想在黑名单里把这些全部堵死几乎是不可能的——因为HTML标准本身还在不断演进今天的新标签新特性明天就可能变成新的绕过载体。我在实际做防护评估的时候最怕看到的就是“我们做了XSS过滤”这种说法。因为过滤方案天然有局限它只能应对已知的攻击模式对未知的、变形的、利用解析差异的攻击几乎束手无策。接下来我会把常见的绕过思路掰开揉碎讲一遍不是为了教你打站而是让你知道如果没有这些认知你写的过滤器约等于在裸奔。2. 过滤器为什么能被绕三种经典的绕过思路拆解2.1 编码绕过理解浏览器“先解码再解析”的顺序特性编码绕过是XSS过滤绕过里最基础、也最实用的一类手法几乎所有公开的payload里都多多少少用了编码技巧。它的核心原理是过滤器在匹配关键词时匹配的是原始字符串而浏览器在执行时会先对HTML实体、URL编码、Unicode编码进行解码再交给解析器处理。这两个步骤之间的时间差就是攻击者的操作空间。举一个最经典的例子假设你的过滤规则是匹配javascript:这个字符串攻击者提交的是javascriptcolon;alert(1)。注意这里的colon;是HTML实体对应冒号字符。过滤器收到的原始数据是纯文本它搜索javascript:发现搜不到因为那里是colon;而不是:于是放行。数据进入浏览器后HTML解析器先把colon;解码成冒号JavaScript引擎才看到完整的javascript:alert(1)于是执行。这个例子说明了一个关键问题单纯的字符串黑名单在浏览器多层解析机制面前是不堪一击的。你不仅要过滤javascript:还要过滤它的HTML实体形态、URL编码形态、Unicode兼容形态比如全角冒号、甚至混合大小写形态JaVaScRiPt:。你可以试试把所有这些形态都写进黑名单写完之后你会发现规则条数膨胀得可怕而且一旦浏览器更新解析规则你又得跟着补。2.2 语法变形绕过过滤器的“关键词匹配”怕什么它就做什么语法变形是另一种绕过滤器的常见思路。过滤器的本质是关键词匹配那么攻击者的思路就是在不改变JavaScript语义的前提下把代码改写成不包含任何关键词的形态。最典型的是字符串拼接alert(1)可以写成alert(1)也可以写成window[alert](1)。如果过滤器禁了alert我可以用top[/al/.source/ert/.source](1)这种正则表达式绕法。如果禁了括号我可以用alert配合throw语句、Function构造器、setTimeout的字符串参数等替代方案。还有更取巧的如果过滤器只查alert(1)这种精确匹配攻击者可以在中间插入注释、换行、Tab比如alert(/*注释*/1)或者alert(\n1)。这些空白字符和注释在JavaScript引擎解析时会被完全忽略但它们在字符串匹配时却能让过滤规则失效。很多初学安全的同学第一次看到这些payload时会被“变形”搞晕但你只要理解了核心逻辑——过滤器在死板地做字符串匹配攻击者在灵活地构造等价语义——你就能明白为什么任何由黑名单组成的过滤方案早晚都会被绕过去。2.3 标签滥用绕过利用“建标签—触发事件”这条通用执行链标签滥用的本质是把“创建HTML元素”和“触发事件处理器”这两件事解耦。攻击者不需要script标签他只需要找到一个能正常渲染的HTML标签再给它挂上一个能自动触发的事件属性就形成了一个完整的攻击链。这里有一个关键点自动触发事件通常依赖两个条件一是元素要被渲染到页面上并且能加载外部资源二是触发事件不需要用户交互也就是不需要点击。符合这两个条件的标签和事件组合非常多比如img srcx onerroralert(1)图片加载失败触发error事件、svg onloadalert(1)SVG加载完成触发load事件、body onloadalert(1)页面主体加载完成触发load事件、videosource onerroralert(1)视频资源加载失败触发error事件。你可能会问那我把所有事件属性名字都过滤掉总行了吧on开头的属性就那么多列出来不就得了理论上可以但实际做起来会发现第一事件属性并不都是以on开头的比如a hrefjavascript:...这种协议执行链就不依赖事件属性第二如果这是一个富文本内容场景你没法简单地把所有on属性禁掉因为很多合法的样式和功能也依赖属性禁用之后用户体验全毁。黑名单方案走到这里就彻底陷入了两难——要么安全要么功能二选一。这也是我为什么一直在跟团队强调防御XSS的治本之道在于“输出编码”和“输入校验白名单”而不在于黑名单过滤。3. WAF绕过从规则盲区到语义分析3.1 WAF的工作机制与绕过思路演变WAFWeb应用防火墙部署在业务的前端本质上是一个深度包检测引擎。早期的WAF规则以正则表达式为主比如匹配script、onerror、union select这些特征字符串。攻击者很快就发现只要稍微变形一下规则就失效了。于是WAF绕过技术跟杀毒软件查杀技术一样进入了一场长期的军备竞赛。WAF绕过的通常套路是分几步走的第一步探测WAF的规则集看哪些关键词、哪些payload会触发拦截哪些不会第二步根据探测结果选择一个能绕过规则的编码方式或者标签变体第三步如果WAF对请求体做了深度解码和还原攻击者再换用分块传输、multipart畸形构造等方式试图绕过解析层。到了今天稍微好一点的WAF都在从“正则匹配”走向“语义分析”。所谓语义分析就是不只是找关键词而是尝试解析请求里的数据流还原出攻击者想表达的真正语义。比如把HTML实体解码后的结果、把多重编码解码后的结果、把JavaScript代码的AST结构都提取出来再做匹配。这样做确实大幅提升了检测能力但它对WAF自身的性能消耗非常大而且仍然存在盲区——任何规则引擎都只能覆盖已知的攻击模式语义分析的模型再完善也跟不上浏览器解析器那套“隐含规则”的复杂度。3.2 WAF绕过实战中的常见盲区从防御视角看做防御的人最忌讳的事情是对自己的防线过度自信。我在帮一些团队做WAF策略评估的时候真的见过不少“裸奔”的案例。下面列几个我印象比较深的盲区都是从攻击者视角总结出来的但目的是让你知道该去补哪里。第一个盲区是多层编码嵌套。有些WAF会对请求做一定程度的解码再检查但只解码一层。攻击者构造双重URL编码、HTML实体加URL编码混合嵌套WAF解码完一层发现不是恶意内容放行浏览器因为自身解析机制又自动解码了一层最终形成了攻击。这是个典型的“解码层数不一致”问题防御方必须明确自己的解码链路和浏览器一致这个一致性往往很难保证。第二个盲区是参数污染。同一个参数提交多个值或者用Content-Type诱导WAF用错解析方式。比如你给WAF说这是application/x-www-form-urlencoded它按这个格式去解析参数但实际请求体是multipart/form-data的格式那么WAF解析出来的参数集合和业务后端解析出来的参数集合是不一致的攻击者可以把自己真正想攻击的payload藏在那个WAF“看不见”的参数里。第三个盲区是协议层面的边界模糊。比如利用分块传输编码chunked transfer encoding处理畸形的chunk大小来干扰WAF的流量重组这个问题现在不少WAF已经能处理了但确实还是一个常见的绕过切入点。业务侧如果不强制校验HTTP协议的规范性这类请求就能穿过大部分规则。3.3 别只指望WAF纵深防御的核心思路我跟很多团队聊过WAF的部署策略有一个问题非常突出大家太把WAF当成“银弹”了。上了WAF就好像安全体系建设完成了后续的开发迭代里也没人再去想XSS的问题。真正的做法应该是纵深防御。WAF只是第一道闸门它在拦截批量扫描和普通攻击者的时候非常有效但你不能让它成为最后一道防线。应用层必须有一套独立于WAF的安全编码规范服务端要对所有输出内容做上下文感知的HTML编码、属性编码、JavaScript编码客户端可以启用严格的CSP内容安全策略来限制脚本来源和允许执行的能力数据库和接口层面要做好输入参数的白名单校验。多层防线各管一段即使某一条防线被绕过后面还有别的防线兜底。这个道理其实一点都不复杂但我发现很多团队在“安全建设”和“业务开发”之间缺少一个翻译层——安全团队提了一堆要求研发团队不知道怎么写代码才叫“安全”。所以我把服务端和客户端常用的一套防御配置直接整理出来放到下一节里你可以当checklist直接用。4. 防御方视角把“绕不过”作为目标搭建多层级XSS防护体系4.1 服务端输出编码上下文感知才是关键如果你问我针对XSS过滤绕过最强力的单点防御是什么我会毫不犹豫地回答服务端对输出做严格、分上下文的编码。所谓“分上下文”意思是根据数据输出的位置选择对应的编码方式。数据是输出在HTML标签之间输出在HTML属性里还是输出在JavaScript代码里编码规则是完全不同的。输出在HTML文本节点时需要对 这几个字符做HTML实体编码防止形成新的标签输出在属性值时除了上述字符还必须对引号和反引号做编码防止它逃逸出属性值的边界输出在JavaScript字符串里时则需要做JavaScript的Unicode转义处理。很多研发团队只写了一个统一的htmlspecialchars()函数转义所有输出结果在JS上下文里这个函数根本不起作用——攻击者的payload只要不被转义成lt;就能在JS执行环境中直接跑起来。这就是我强调“上下文感知”的原因。你可以在服务端封装一个工具类核心接口是htmlEncode()、attrEncode()、jsEncode()、urlEncode()在模板渲染时严格按位置选对编码器。这是最枯燥但最有效的防御手段它不依赖任何规则库不依赖WAF的判断能力从源头上杜绝了输入变成代码的可能。4.2 客户端CSP策略给浏览器上“紧箍咒”CSPContent Security Policy是一个经常被低估的防御手段。它的思路不是阻止恶意脚本进入页面而是限制进入页面的脚本能不能执行。你可以在响应头里加上Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self这一行配置的含义是页面里的脚本只能从同源地址加载不允许内联脚本不允许eval()及相关函数不允许object、embed、applet等插件标签。如果攻击者注入了scriptalert(1)/scriptCSP会直接阻止这条内联脚本的执行。就算攻击者把payload变形出花来只要没有外部可控的JS文件加载他的代码就执行不了。在实际落地时CSP会带来一些“麻烦”比如前端如果大量使用内联脚本、动态执行eval()上线之后会被CSP误伤。这时候比较好的做法是分阶段推进先开启Content-Security-Policy-Report-Only模式让浏览器把违规行为上报而不是直接拦截按报告把不合规的代码改造掉然后再切到强制拦截模式。这个过程需要前端同学配合但收益是实打实的——它是目前对注入类攻击最有效的浏览器原生防线。4.3 输入校验白名单比黑名单务实一百倍的思路黑名单的替代方案是白名单。落地到实际研发规范里就是能限定类型就限定类型能限定格式就限定格式实在不能限定的统一按“不确定内容”处理交给输出编码兜底。比如一个手机号输入框后端就应该只接受数字、加号和少量分隔符其他字符一律拒绝一个年龄字段就应该只接受0到150的整数一个用户昵称如果允许任意字符那后端在输出时就一定要走HTML实体编码。这个逻辑看起来简单但很多团队在设计接口的时候压根没定义字段的合法格式什么字段都是“字符串”让数据在系统里自由流通最后到前端渲染时已经分不清哪些内容是可信的、哪些是不可信的。我建议在做系统设计评审的时候安全人员一定要参与重点就盯一件事每个字段的合法值域是什么。这个值域定义得越清晰XSS的利用面就越小。如果字段值域根本没法定义那这个字段就不该由用户直接输入应该走富文本编辑器方案用白名单解析器过滤标签和属性而不是自己写正则硬匹配。4.4 富文本场景的XSS处理标签级白名单解析器富文本是XSS过滤绕过的重灾区。用户要传富文本你不能把所有HTML过滤掉但又不能让img onerroralert(1)这种payload执行。这时候就需要一个基于标签属性的白名单解析器比如DOMPurify这样的库。这类解析器的核心思想是先把用户提交的HTML字符串放进一个独立的DOM解析器里浏览器自己把这个HTML解析成一棵DOM树然后白名单解析器遍历这棵DOM树把不在白名单里的标签直接删除把不在白名单里的属性删除把属性值里的危险协议比如javascript:过滤掉最后再序列化输出一个“安全的HTML”。因为解析过程运行在真实的浏览器环境里不存在“过滤器以为的HTML结构”和“浏览器实际解析的HTML结构”不一致的问题所以这类方案在对抗标签绕过时表现非常稳定。我现在做架构评审时只认可三种富文本处理方案服务端用白名单解析器清洗、客户端用DOMPurify清洗后提交、或者走markdown格式限制标签范围。凡是用“写了几个正则过滤掉script标签”来支撑富文本需求的方案我都会打回去重写。5. 常见问题与排查技巧实录5.1 上线了WAF和过滤器为什么还是被打穿这个问题我被问过太多次了。排查思路其实是固定的先搞清楚攻击请求有没有到达业务服务器。看WAF的访问日志和拦截日志如果连日志里都没有说明攻击请求走的链路压根不在WAF的检测范围内——常见原因有没接入HTTPS流量、只防护了部分域名、CDN回源绕过了WAF、或者WAF部署在架构里但在其他接入层之后。确认请求到达了业务服务器后再往业务侧排查这个请求落到哪个接口、最终被渲染到哪个页面位置、服务端有没有做输出编码、是不是有一段逻辑“信任”了这个输入导致没有编码。我见过太多例子最后定位到的原因非常朴实研发同学为了方便把用户输入直接拼接进了某个innerHTML的赋值语句而服务端的统一输出编码根本没覆盖到这一行。所以排查时不要只盯着安全设备代码层的修复才是根治。5.2 如何评估当前过滤规则的强度我在做安全评估时不会问“你们做了过滤吗”而是会直接看防守方自己有没有能力回答这几个问题你们过滤规则针对哪几种上下文是否覆盖HTML实体编码、URL编码、Unicode编码是否对属性上下文单独处理是否覆盖了iframe的srcdoc、svg的animate、math的mtext这些冷门但可用的标签如果答案都是“不确定”那基本可以判断这套过滤方案的有效性堪忧。快速验证的方法也很简单拿几个经典的payload变体实际打一下scriptalert(1)/script、img srcx onerroralert(1)、svga xlink:hrefjavascriptcolon;alert(1)、mathmtexttablemglyphstyle!--/styleimg title--img src1 onerroralert(1)看每条是否被准确拦截。这个测试倒不是鼓励你去学怎么攻击而是让你对自家防御的真实水平心里有底。5.3 WAF误报太高业务天天来投诉怎么办误报是WAF运营里最头疼的日常。我见过一个案例某个WAF规则把包含select标签的SQL模板全部拦截了导致后台一个合法的配置页面直接无法打开。处理这类问题的经验是三层第一层WAF规则要有精细的“域名—路径—接口”白名单能力核心业务接口可以单独配置检测粒度第二层误报拦截要能自动生成处置工单而不是让业务人员自己去翻日志猜原因第三层规则运营要从“命中即拦截”改成“先观察后拦截”新规则先在监控模式跑一段时间确认误报率可控后再打开拦截。误报问题说起来是个技术问题但本质上是个规则运营成熟度的问题。规则不是越多越好关键是要针对真实攻击面来做精配。你把自己的核心业务接口梳理清楚每个接口的参数结构、合法值域都清楚了再针对性地写检测规则效果远好过导入一份上千条的通杀规则库。5.4 开发同学问“这段代码到底安全吗”我给的统一建议开发同学经常拿着代码片段来问我“这段有没有XSS”。我的回答一般是一个流程第一步看这个变量值能不能被用户控制能就假设它不可信第二步看它输出在什么位置是HTML标签内、属性内、还是JavaScript代码内第三步确认该位置是否做了对应的输出编码第四步看有没有绕过CSP的内联脚本/事件属性存在。这四个问题问完代码的安全水平就判断个八九不离十了。如果代码里用了富文本直接上白名单解析器如果用了前端模板渲染确认模板库默认转义开启如果用了href、src属性拼接确认协议被限制为http和https如果用了eval、setTimeout(字符串)、document.write、innerHTML这四类高危API无条件重构或者改成安全替代方案。这套清单非常朴素但它能拦住绝大多数实际发生的XSS漏洞——因为真实的攻击大多数都藏在看似不起眼的编码遗漏里而不是什么高深的0day技巧里。6. 最后聊点实际的我做了这么多年的安全防护最大的感触就是XSS过滤绕过这个问题没有一劳永逸的答案。标签绕过的手法会跟着HTML标准的演进不断更新WAF绕过在攻防对抗中也会持续进化今天你觉得万无一失的规则明天可能就被一个冷门编码绕过去。但这不代表防御方就无能为力恰恰相反正因为攻击手法会变所以我们才要把防御体系建立在那些不变的原理之上。什么是不变的原理输入永远不可信、输出必须按上下文编码、脚本执行能力必须最小化限制、安全能力要分层部署而不是单点依赖。把这些原理固化成编码规范、架构规范和上线检查项比追着每一个新的绕过手法打补丁要靠谱得多。我个人在实际落地时还有一个习惯每季度拿真实业务的页面做一次XSS专项自查直接把上一季度收集到的攻击payload变体跑一遍看防御策略有没有失效。这个动作看起来不起眼但它能最直观地反映安全水位的变化。如果你的团队连这个自查都没有那我建议从下个季度开始试试跑一遍你心里就有底了——到底是纸面安全还是真刀真枪扛得住一试便知。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →