我们先把XSS这回事说透:它本质上不是服务器被"打"了,而是攻击者往你的网页里注入了一段浏览器能执行的脚本,让每个打开页面的用户都变成"受害者"。WAF站的位置很特殊——它挡在真实用户和你的Web应用之间,所有HTTP请求都会从这里过一道关卡。所以WAF能不能拦住XSS,直接决定了你的站点是被当成"示范案例"挂在漏洞库里,还是沉默地扛住一整年的恶意扫描。
这篇内容我按"攻击本质、防护机制、规则配置、问题排查"四个层次来拆,适合三类人看:一是刚接手公司WAF运维的同学,想搞明白规则面板里那些参数到底在做什么;二是被XSS漏洞折腾过的开发,想知道怎么在不动代码的前提下先止损;三是做安全评估的人,需要理解WAF绕过测试的思路,避免盲目下单买东西。我不讲厂商专属的功能,也不会丢一堆让你更懵的缩写,只讲原理和操作,你换个牌子也一样能用。
1. XSS攻击到底在打什么?WAF为什么要管这档子事
1.1 三种XSS形态和它们真正瞄准的目标
很多人把XSS当成"往输入框里塞一串script标签"那么简单,实际操作中,它分三条截然不同的路子:
存储型XSS是最麻烦的。攻击者把恶意代码写进数据库,比如评论区、用户昵称、订单备注,之后任何一个用户刷新了带这段内容的页面,脚本就跟着跑一遍。它不需要骗用户点链接,攻击面是"所有访问页面的人"。
反射型XSS讲究的是"借刀杀人"。恶意代码藏在URL参数里,攻击者把拼接好的链接发给受害者,受害者点开时,服务器把参数原样吐回页面,脚本就在他的浏览器里执行了。这种类型的生命周期极短,但非常适合钓鱼和身份盗用。
DOM型XSS更隐蔽,它绕过了服务器端,攻击者改的是前端JavaScript的运行逻辑,数据从来不经过后端接口,直接在浏览器里完成的注入。这就导致传统日志分析、后端WAF的检测非常容易漏掉它,因为它看起来就是一个再普通不过的合法请求。
真正值得警惕的是,XSS最终的破坏力不取决于攻击代码的外观,而取决于它能调用什么权限。攻击者拿到的是"用户浏览器里的会话",能干的事情包括偷cookie、改页面内容、模拟用户发帖、内网探测。在我的实际测试经验里,一个存储型XSS在大型应用中的影响半径,往往比一个SQL注入还要大,因为它能直接操纵"人"的行为。
1.2 WAF在安全体系里的定位:不是银弹,但必须是第一道闸门
WAF解决的是一个很现实的问题:开发团队修代码需要时间,而攻击者不会等。你用框架自带的安全函数能解决80%的问题,但业务逻辑越复杂,拼接HTML、动态渲染JS的地方越多,漏网的可能性越大。WAF作为一道独立于应用逻辑的过滤层,能拦截那些"看起来就不像好人"的请求。
我用一个比较接地气的类比:WAF就像小区门口的保安。他不需要知道你家里具体摆了什么家具,但他知道"一个人扛着撬棍上门"这种事不对劲。他要做的是在进门之前拦住可疑的人和包裹,而不是等你家被搬空了才报警。
不过必须强调,WAF不能替代代码层的修复。它擅长的是在短期内建立防线,给你争取修复时间,同时挡住大多数自动化攻击和批量扫描。如果你的业务里存在富文本编辑器、Markdown渲染这类"需要接受任意HTML"的场景,WAF的配置复杂度会直线上升,这时候光靠拦截规则不够,还得配合白名单和内容安全策略一起用,后面我详细展开。
2. WAF防XSS的三板斧:规则引擎、语义分析、行为识别
2.1 基于规则的特征匹配:最传统,但依然是地基
几乎所有商业WAF和开源方案(比如我调试过的各种社区版)的底层,都保留了一张庞大的特征规则库。检测的原理很简单:把请求里的参数值、路径、头信息,甚至POST的原始报文体,拿出来和成千上万条正则表达式做比对。
典型的高危特征包括:
- 标签类:
<script、<iframe、<object、<embed - 事件属性类:
onload、onerror、onclick、onfocus - 危险函数调用:
javascript:协议、eval(、document.cookie、XMLHttpRequest - 编码混淆特征:
j这种HTML实体、%27这种URL编码、\x3c这种JavaScript字符串转义
这套机制最大的优点就是快和直观。流量经过时做一次正则扫描,毫秒级就能出结果,非常适合挡住那些扫描器批量丢过来的基础payload。我自己在测试环境里试过,一个默认配置的WAF应对sqlmap和常见XSS扫描payload,拦截率可以做到90%以上。
但它的问题同样明显:正则永远追不上攻击者的变种速度。攻击者把<script>改成<scr\nipt>,把alert(1)改成alert反引号1反引号,正则一不留神就放过去了;反过来,一个正经程序员在博客里写了篇文章,里面全是"javascript:"代码示例,规则库会误判成攻击,把正常访问给拦了。
所以,仅靠规则匹配的WAF,在对抗有针对性攻击的时候,力不从心;在业务复杂的站点上,误报率也高到让人头疼。
2.2 语义分析与上下文感知:让WAF真正"读懂"请求
这是近些年WAF产品都在宣传的核心能力,也是我觉得最值得理解的一块。它的原理是:不再把请求当成一堆字符串去匹配,而是把它解析成一棵语法树,再判断各个节点的位置是否合理。
举个例子,同样是字符串你好<img src=x onerror=alert(1)>:
- 如果这个参数是放在URL的查询字符串里,WAF可能需要先解码,再判断它是否会被拼接到HTML中;
- 如果这个参数是在JSON体里,并且对应的接口只是存储用户昵称,后端大概率不会把它当HTML渲染,那么拦截优先级就可以降低;
- 如果参数是出现在
src属性引号内部,攻击者可以尝试闭合引号再加入新属性,语义分析的引擎会追踪这个"引号闭合再注入"的模式。
语义分析带来的直接好处是误报率显著下降。我的一个客户曾经被传统规则误伤过——他们的产品详情页允许用户输入"<b>加粗</b>"样式的文本标签,被老式规则库直接封禁,业务方急得跳脚。换成带语义引擎的防护之后,WAF能够区分"文本里的HTML示例"和"真正要执行的事件属性",业务正常放行,恶意构造依旧拦截。
但语义分析也非没有弱点。它非常消耗性能,开启完整检测可能导致高峰期CPU上涨10%到20%;而且攻击者可以用多级嵌套的编码方式故意让解析器看不懂,毕竟解析器的能力上限,就是它训练数据的质量上限。
2.3 基于行为与基线的异常识别:对付"慢速变异"的补充手段
有的攻击者很聪明,他知道规则库更新需要时间,就故意放慢节奏,把一次攻击拆成几十个请求,每个请求都只携带一个看似无害的片段。比如第一次发<scr,第二次发ipt>,第三次发onload。这样所有得分都不超过拦截阈值,规则匹配完全失效。
行为分析模块这时候就起作用了。它会记录同一个IP或者同一个会话在过去一段时间内的请求序列、参数分布、访问路径模式,然后和"正常基线"做比对。出现下面几种情况就要告警:
- 同一个客户端在短时间内,对大量不同URL发出结构类似的请求;
- 某个参数值里面连续多次出现概率极低的字符组合,比如尖括号加引号加百分号编码的混合体;
- 一个从未出现在某个API上的User-Agent,突然开始高频调用带大量参数的接口。
行为识别的优势是能捕捉"缓慢试探"的攻击节奏,但也容易误伤。我在实际调参中见过最典型的案例:一个MOBA游戏数据站,玩家会高频刷新比赛结果页面,参数里面带了很多百分号编码的时间戳和队伍信息,行为模型把它们当成攻击,害得玩家大面积掉线。后续我把"频率阈值"和"参数熵值"调宽了两倍,才平息了一场事故。
3. 实操:从零配置一套XSS防护规则
3.1 预处理阶段:规范化是命根子,解码顺序错一步就全错
我在接入任何WAF产品时,第一件事不是去开特征库,而是把"预处理"里所有解码选项全部打开。所谓的预处理,就是在真正匹配规则之前,先把原始流量做一层归一化处理:
- 对URL做百分号解码,
%3C变回< - 对HTML实体做解码,
<变回<,j变回j - 对Unicode做标准化,把全角、半角、异体字归一
- 对压缩的内容解压(有些攻击者用gzip包裹payload来绕过)
- 对JSON、XML、表单等不同格式的数据做结构化解包
这一步的重要性怎么强调都不过分。因为90%的WAF绕过都是靠编码层面的花活实现的。攻击者把<script>编码成%253Cscript%253E,如果你的解码顺序是"只解一层URL",得到的还是%3C,规则库自然识别不出;只有做递归解码,把两层、三层的编码全部还原,才能真正看到攻击原型。
我建议你在配置完成之后,做一次"编码递归测试"。直接构造一个双重编码的测试包打过去,看日志里是否留有解码后的明文记录。如果日志里显示的是编码前的原始值,那你的解码链一定是断了。
3.2 规则引擎配置示例:用自己的逻辑拦截一次XSS
这一节我用一种与厂商无关的通用规则语言来描述,你在实际平台里找到类似的函数就能套。
第一步,创建一个"XSS高危特征"规则组,优先级打最高:
# 示意配置,请根据实际WAF产品调整 SecRule ARGS "@rx (?i)<script" \ "id:1000,phase:2,deny,status:403,\ msg:'XSS Script Tag Detected',\ logdata:'Matched in %{MATCHED_VAR_NAME}'"这条规则的意思是:对所有参数值做正则匹配,忽略大小写,一旦看到<script就拒绝请求并返回403。它的作用是拦截最常见的script注入。
第二步,针对事件属性单独建组,因为很多WAF设计师倾向于把事件属性和标签分开处理,这样能精细控制误报:
SecRule ARGS "@rx (?i)on(load|error|click|mouseover|focus|blur)\s*=" \ "id:1001,phase:2,deny,status:403,\ msg:'JavaScript Event Handler Found',\ tag:'XSS'"第三步,针对参数中出现javascript:协议的情况,尤其是出现在链接、表单、iframe中的场景:
SecRule ARGS "@rx (?i)javascript\s*:" \ "id:1002,phase:2,deny,status:403,\ msg:'javascript: URI Detected'"这里有一个很关键的边界问题:如果你用这套规则去保护一个允许用户发帖的论坛,那用户发一段"我学习javascript的一天",就会撞上第三条规则被误杀。所以我在生产环境里不会直接拒绝,而是先"记录、放行、打标签",观察几天日志,再逐步把规则从deny收紧到deny,从全局作用域缩小到指定路径。
3.3 业务兼容与白名单:别让WAF变成最大的"业务中断器"
在金融、电商、内容社区这些业务场景里,富文本编辑器的存在意味着总有合法的请求包含HTML标签。如果把规则拉满,结果就是用户发了篇图文并茂的文章,直接被拦成403,客服电话被打爆。
我的推荐做法是三段式策略:
第一段,全局开启"防护模式",记录可疑请求但不阻断,持续收集一周的样本;第二段,针对业务路径做精细化规则拆分,比如/article/publish这个接口允许有限的HTML标签,对<img>的src属性做白名单域校验,对<a>的herf禁止javascript:;第三段,针对正常业务操作中产生的"类XSS"文本,比如开发文档里示例代码、数学公式编辑器生成的LaTeX,加入正则白名单。
我遇到过最典型的案例是某产品的"代码分享"功能,用户要提交带<div>、<style>的代码片段。如果一刀切拦截,整个功能就废了。后来我们的方案是:要求提交时前端先把代码做base64编码,后端WAF识别到这个包装结构之后只做格式校验,不深入扫描内部内容,同时在服务端调用语法树解析器,把真正的HTML标签转换为纯文本返回。这样既给了功能生存空间,也把攻击面收敛到了可控范围。
4. 常见绕过和误报问题排查实录
4.1 绕过手段解剖:为什么你说的"拦住了"其实没拦住
我整理过一份内部WAF绕过测试清单,每次上线前都会跑一遍,这里挑几个压箱底的例子说说。
第一种是标签拆分。<script>被拼成<scr<script>ipt>。如果WAF只做了一次正则提取,看到中间的<就乱了;但渲染时浏览器会合并标签,最终执行结果和完整标签一模一样。
第二种是属性闭合。经典payload像"><svg/onload=alert(1)>,利用了引号错误引发的事件属性注入。很多规则只盯着onload=,如果攻击者换成onpointerenter、onanimationstart这些冷门事件,规则库没收录就直接漏过。
第三种是编码叠buff。攻击链是'><svg onload=alert(1),它把HTML实体、十六进制、URL编码混在一起,如果解码逻辑只做一遍,拿到的还是"看起来很正常"的字符串。
应对这些绕过的核心不是扩展几百条正则,而是强制WAF走"完整解码-语义分析-上下文检测"的流水线。比如在语义分析里,引擎能把<svg/onload=alert(1)>识别为"svg标签内绑定了手写事件处理器",这在业务逻辑里完全不应该出现。
4.2 误报排查速查表:用户的正常操作,为什么被WAF按住了
误报比漏报更让运维头大,因为漏报往往是后知后觉,误报是立刻杀死业务。我把实际排查过程中遇到的误报型态整理成了一张表:
| 误报场景 | 触发原因 | 排查定位方法 | 处置方案 |
|---|---|---|---|
| 热点新闻原文包含" |