简介:这份PPT面向Web安全从业者、运维工程师与安全架构学习者,系统梳理WAF从产品形态到架构落地的完整脉络,帮助读者理解如何为Web应用构建有效的攻击防护体系。内容覆盖硬件盒子、软件WAF与云WAF三类产品形态及代表厂商,重点拆解云WAF的流量接入、HTTP(S)反向代理与安全规则引擎三大基础要点,并对比DNS调度、CNAME与Anycast等接入方式的优劣。同时深入Nginx/OpenResty扩展机制、ModSecurity CRS规则集与Naxsi自主学习引擎的选型思路,以及PASS、DENY、ALLOW等动作配置与匹配条件写法,兼顾原理与实操。资源包共1个pptx文件,约3.93MB,页面结构清晰,适合按章节对照学习。目前已有314人学习,可作为安全岗位面试准备或WAF方案设计的参考材料。
1. WAF 的真相:为什么正则规则库注定漏报,而语义分析才是分水岭
很多人对 WAF 的第一印象是「一堆正则表达式拦 SQL 注入」,这个认知本身就是最大的坑。正则匹配的是字符串形态,攻击者只要把union select拆成union/**/select、做一次 URL 编码、或者用大小写混写,规则库里那条精心写好的正则就失效了。WAF 的实现与架构演进,本质上就是围绕「如何不依赖字符串形态去识别攻击意图」这条主线展开的。从硬件盒子到云 WAF,从 ModSecurity 的正则规则集到 libinjection 的语义分析,每一次架构变化背后都是同一个追问:检测引擎到底该在哪个层次上理解流量。搞懂这条演进线,你才能明白为什么有些 WAF 看起来规则很全却总被绕过,也才能判断一套规则引擎在真实流量下会不会翻车。
2. 流量接入层:DNS 调度和 Anycast 到底在解决什么问题
WAF 要生效,第一件事不是检测,而是让流量先经过它。这一步在云 WAF 里叫流量接入,是整个架构的地基。地基没打好,后面规则引擎再强也没用——流量根本没进来,检测个寂寞。这一层要解决的核心矛盾是:如何在不改动用户源站架构的前提下,把原本直连源站的请求「骗」到 WAF 节点上,同时还要保证调度灵活、故障能自动切换。
2.1 CNAME 接入与 DNS 调度的本质
最常见的接入方式是 CNAME。用户把域名解析指向 WAF 厂商提供的一个别名,DNS 查询时这个别名再解析到具体的边缘节点 IP。这个机制的关键在于:控制权在 DNS 解析结果上。厂商想把你调度到哪个机房,改一下 CNAME 背后的解析记录就行,源站完全无感。
DNS 调度能做的事情比想象中多。按地域分流,北京用户解析到北京机房、广州用户解析到广州机房,降低回源延迟;按线路分流,联通用户走联通线路的节点、电信用户走电信节点,避免跨网抖动;按负载分流,某个机房被打爆时把一部分解析结果切到空闲机房。这几种调度策略在实现上是同一套机制的不同参数配置,本质都是「根据请求来源特征返回不同的 IP 集合」。
但 DNS 调度有个绕不开的短板:生效时间受 TTL 制约。你把 TTL 设成 60 秒,理论上最多一分钟生效,但现实中各地递归 DNS 的缓存行为参差不齐,运营商 LocalDNS 不遵守 TTL 是血泪经验里最常见的一条。攻击流量打进来时,你改了解析,可能几分钟内还有大量请求打在旧 IP 上。这就是为什么纯 DNS 调度的云 WAF 在应对突发大流量时,切换总是不够干脆。
2.2 Anycast:把调度下沉到路由层
Anycast 换了个思路:不靠 DNS 返回不同 IP,而是让多个边缘节点宣告同一个 IP 地址段,由 BGP 路由协议决定流量走哪个节点。用户解析出来的 IP 就是边缘节点 IP,但这个 IP 在全球多个机房同时存在,网络层会自动把包送到「路由上最近」的那个节点。
这个机制带来的好处很直接。路由策略变动生效快,BGP 收敛通常在秒级到几十秒级,远优于 DNS 缓存的不确定性;调度发生在路由层,对 DNS 解析性能没有额外要求,也不怕 LocalDNS 乱缓存;单点故障时,节点撤销路由宣告,流量自动被 BGP 重新收敛到其他节点,不需要人工改解析。
代价也很实在。Anycast 要求厂商拥有整段 IP 地址的控制权,一个 C 段起步,IP 资源浪费严重;全球 BGP 路由错综复杂,优化路由策略是件极其繁琐的工程,稍有不慎就会出现「本该走 A 节点的流量绕到了 B 节点」;而且 Anycast 的「最近」是路由跳数意义上的近,不一定是物理距离或网络质量意义上的近,需要持续做路由调优。
提示:判断一个云 WAF 的接入质量,不要只看它支持不支持 CNAME,要看它在 DNS 调度之外有没有路由层调度能力。纯 DNS 方案在故障切换场景下的表现,和 Anycast 方案不在一个量级。
2.3 反向代理:为什么 OpenResty 成了事实标准
流量接入之后,WAF 节点要以反向代理的身份终结客户端连接、再向源站发起新连接。这一层选型直接决定了检测引擎能拿到什么、能改什么。Nginx 是主流选择,原因有三:网络模型效率高,epoll 加多进程能充分利用多核;模块化设计,扩展性强;除了四层七层代理还有 Web 服务器和缓存能力,一个组件干多件事。
但真正让 OpenResty 成为 WAF 事实标准的是 Lua 扩展能力。Nginx 原生模块要用 C 写、编译进二进制,改一次逻辑就得重新编译部署。OpenResty 把 Lua 嵌进 Nginx 的各个执行阶段,规则逻辑可以用脚本写、热加载、按需调整。对 WAF 这种规则频繁迭代的场景,这个差异是决定性的。
OpenResty 的执行阶段划分值得单独理解,因为它决定了 WAF 能在请求生命周期的哪个点介入:
| 阶段 | 典型用途 |
|---|---|
| ssl_certificate_by_lua | SSL 证书动态处理,SNI 分流 |
| set_by_lua | 变量初始化 |
| rewrite_by_lua | 转发、重定向、缓存判断 |
| access_by_lua | 请求头与参数检测,WAF 主战场 |
| content_by_lua | 内容生成 |
| header_filter_by_lua | 响应头过滤 |
| body_filter_by_lua | 响应体过滤,防敏感信息泄露 |
| log_by_lua | 会话结束后异步记日志 |
WAF 的请求检测主要落在 access_by_lua 阶段,此时请求头和参数都已解析完毕、尚未转发给源站,是拦截的最佳时机。响应方向的检测则落在 body_filter_by_lua,用来做响应体脱敏或防网页篡改。理解这套阶段模型,你才能明白为什么基于 OpenResty 的 WAF 能在不改动 Nginx 核心的前提下实现完整的检测链路——它不是在代理之外挂了个检测器,而是把检测逻辑编织进了代理的每个生命周期节点。
3. 规则引擎:ModSecurity 的正则路线和 libinjection 的语义路线
检测引擎是 WAF 的大脑,也是架构演进中分歧最大的地方。同样是拦 SQL 注入,ModSecurity 走的是「规则加正则」,libinjection 走的是「词法语义分析」,Naxsi 走的是「自主学习生成规则」。三条路线的取舍,决定了它们各自能防什么、漏什么。
3.1 ModSecurity CRS:规则全面但依赖形态匹配
ModSecurity 的核心是规则集,CRS(Core Rule Set)是其中使用最广的一套,覆盖 OWASP Top 10 的常见威胁。一条规则的骨架包含几个要素:匹配哪些变量、对变量做什么转换、用什么操作符、匹配什么模式、命中后执行什么动作。
变量组决定了检测的数据来源,常见的有 REQUEST_METHOD、REQUEST_URI、ARGS、HTTP_USER_AGENT、HTTP_REFERER、REMOTE_ADDR、REQUEST_HEADERS、RESPONSE_HEADERS 等。转换函数在匹配前对变量值做归一化处理,这一步是正则路线能否生效的关键——攻击者做 URL 编码、大小写混写、插入注释,转换函数负责把这些变形还原成规则能匹配的形态。常见的转换包括 uri_decode、base64_decode、html_decode、lowercase、compress_whitespace、remove_comments、remove_nulls 等。
操作符决定匹配方式,除了 regex 正则,还有 contains、begins_with、ends_with、equal、num_range、ip_utils 这类结构化匹配,以及 detect_sqli、detect_xss 这两个语义检测操作符。注意后两个——它们内部调用的正是 libinjection 和 libhtmlentities,也就是说 ModSecurity 在正则之外,也接入了语义分析能力,只是把它包装成了一个操作符。
动作决定命中后的行为:PASS 跳过当前阶段的后续规则、DENY 阻断请求、ALLOW 跳过所有检查、REDIRECT 重定向、RESET_CONNECTION 重置连接。这套动作模型的设计动机是让规则可以分层组合——先放行白名单,再逐条检测,命中即阻断。
正则路线的根本局限在于:规则是「已知攻击形态」的枚举。CRS 有几百条规则,覆盖很全,但每一条都对应一种具体的字符串模式。攻击者只要构造出语义等价但形态不同的 payload,规则就匹配不上。这不是规则写得不好,而是正则这个工具本身的表达边界——它匹配的是字符序列,不是语法结构。
3.2 libinjection:把 SQL 注入当成语法问题
libinjection 的思路完全不同。它不匹配具体字符串,而是对输入做词法分析,把 payload 拆成 token 序列,再判断这个序列是否符合 SQL 语句的语法特征。比如1' or '1'='1会被识别为「字符串字面量 + 逻辑运算符 + 字符串比较」的结构,这种结构在正常业务参数里几乎不会出现,但在注入 payload 里是典型形态。
这个机制的优势在于对变形不敏感。攻击者把空格换成注释、把关键字拆开、做编码,词法分析器在归一化之后看到的 token 序列是一样的,语义判断依然成立。这就是为什么说语义分析是分水岭——它检测的是「这段输入像不像 SQL 语句」,而不是「这段输入里有没有某个关键字」。
libhtmlentities 是同一思路在 XSS 上的应用,对 HTML 实体做解析和归一化,识别标签、属性、事件处理器等结构特征。两者组合,构成了 ModSecurity 里 detect_sqli 和 detect_xss 两个操作符的底层实现。
3.3 Naxsi:不依赖规则库的自主学习
Naxsi 走的是第三条路:不维护攻击特征库,而是学习正常请求的参数形态,建立白名单模型,偏离模型的请求就判定为可疑。它的设计动机很明确——未知威胁没有特征,规则库天然防不住,那就反过来定义「什么是正常」,剩下的都当异常处理。
这条路线的代价是误报率和调优成本。正常业务的参数形态千差万别,白名单模型需要持续训练和人工确认,上线初期误拦是常态。它适合参数结构相对稳定的场景,不适合参数自由度极高的业务。
三条路线的取舍可以这样理解:ModSecurity CRS 覆盖面广、开箱即用,但对变形攻击的抵抗力依赖转换函数的完备性;libinjection 对已知类别的变形攻击抵抗力强,但只覆盖 SQL 注入和 XSS 两类;Naxsi 理论上能防未知威胁,但工程落地成本高。实际生产里常见的做法是组合使用——CRS 打底做广覆盖,语义检测做关键类别的深度兜底。
4. 把 ModSecurity 规则机制用 Lua 重写:OpenResty 集成的实现细节
ModSecurity 是 C 实现的,直接嵌进 OpenResty 要么走 FFI 调用、要么走独立进程通信,前者有稳定性风险、后者有性能损耗。一条常见的工程路径是用 Lua 重新实现 ModSecurity 的规则机制,把规则定义、变量解析、转换、操作符匹配这套模型在 OpenResty 里重建一遍。这样规则配置的语义和 CRS 保持兼容,但执行完全在 Lua 层,能充分利用 OpenResty 的协程模型。
4.1 规则的数据结构:match 数组的与或逻辑
一条规则的核心是 match 条件,它是一个数组,数组内多个元素之间是「或」的关系,单个元素内部的多个字段之间是「与」的关系。这个结构设计让一条规则可以表达「URI 或参数里出现某特征,且同时满足另一个条件」这类复合判断。
-- 单条规则的 match 结构示意 local rule = { action = "DENY", -- 命中后的动作 match = { { vars = { -- 数组内多个变量是「或」关系 { var = "ARGS", -- 变量名,对应请求参数集合 parse = { values = true, -- 取出所有 value,parse 内只能出现一种 }, }, }, transform = { "uri_decode", "lowercase" }, -- 转换函数可多选,按序执行 operator = "detect_sqli", -- 操作函数 pattern = nil, -- 操作参数,语义检测不需要 op_negated = false, -- 是否取反 }, { vars = { { var = "REQUEST_METHOD", parse = { specific = "POST" } }, }, operator = "equal", pattern = "POST", }, }, }这段结构里几个字段的含义需要说清楚。vars是数组,多个变量之间取「或」——只要任一变量命中,这个 match 元素就算命中。parse决定从变量里取哪部分数据:specific取指定 key、ignore排除指定 key、keys取所有 key、values取所有 value、all取 key 和 value,这五种只能选一种,因为它们的语义互斥。transform是转换函数数组,按顺序依次执行,前一个的输出是后一个的输入,这个链式设计让「先解码再转小写再压缩空格」这类组合可以声明式表达。operator是最终匹配函数,pattern是它的参数,op_negated控制结果取反。
外层match数组的多个元素之间是「与」——所有元素都命中,整条规则才命中。这个与或嵌套的结构,是规则表达力的来源。
4.2 转换函数链与操作符的配合
转换函数和操作符的配合是检测效果的关键。举个具体场景:攻击者发送%55%4E%49%4F%4E%20%53%45%4C%45%43%54,这是UNION SELECT的 URL 编码。如果规则直接对原始值做 regex 匹配union\s+select,匹配不上。正确的做法是先过uri_decode还原成UNION SELECT,再过lowercase转成union select,最后用 regex 或 detect_sqli 匹配。
-- 转换链的执行逻辑示意 local function apply_transforms(value, transforms) for _, fn in ipairs(transforms) do value = TRANSFORM_FUNCS[fn](value) -- 按声明顺序依次变换 if value == nil then return nil -- 任一转换失败则整条链失败,视为不命中 end end return value end这里有个容易踩的细节:转换链里任一函数返回 nil(比如 base64_decode 遇到非法输入),整条链就中断,规则视为不命中。这个设计是为了避免在畸形输入上产生误判,但也意味着攻击者可以用畸形编码让转换失败、从而绕过检测。常见的缓解思路是在转换失败时记录日志并单独告警,而不是静默放过。
操作符层面,除了 regex,detect_sqli和detect_xss直接调用语义分析库,ip_utils支持子网掩码、单 IP、IP 段三种写法,num_range支持[10, "13", "32-126"]这种混合范围表达。这些结构化操作符的存在,让规则不必所有判断都退化成正则,减少了正则回溯带来的性能风险。
4.3 变量解析的性能考量
parse的取值方式对性能影响很大。values = true要遍历参数集合的所有 value,all = true要同时取 key 和 value,在参数数量多的请求上开销明显。规则设计时应该尽量用specific精确取需要的 key,而不是无差别all。这个取舍在规则数量少的时候感知不到,规则上到几百条、QPS 上到几万时,差异会直接体现在 CPU 占用上。
5. 避坑与排查:WAF 上线后最常见的几类翻车
5.1 规则命中但没阻断,请求照样到源站
现象:日志里规则明明命中了,动作也配了 DENY,但源站还是收到了请求。原因通常是动作执行阶段不对——规则配在了 rewrite_by_lua 阶段,而请求体检测需要读 body,此时 body 还没读完,检测基于空值通过。解决:把需要读请求体的规则放到 access_by_lua 阶段,并确认lua_need_request_body已开启,否则 body 读不到。
5.2 转换链顺序写反导致漏报
现象:明明加了 uri_decode,编码攻击还是绕过了。原因:转换链里 lowercase 写在了 uri_decode 前面,先转小写再解码,解码出来的大写字符没被归一化。解决:转换顺序要按「先解码、再归一化、最后匹配」的原则排,解码类函数永远放最前面。
5.3 白名单规则把攻击一起放行了
现象:为了压误报加了一条 PASS 规则,结果某个接口的注入攻击全放行了。原因:PASS 的语义是跳过当前阶段后续所有规则,粒度太粗。解决:白名单尽量用精确的变量加操作符组合收窄范围,或者用 ALLOW 配合更细的条件,不要用宽泛的 PASS 一把梭。
5.4 Anycast 节点故障后流量没切走
现象:某个边缘节点挂了,部分用户持续访问失败。原因:节点故障但 BGP 路由宣告没撤销,流量还在往死节点送。解决:健康检查要和路由宣告联动,检测到节点不可用时主动撤销宣告,而不是等 BGP 自然收敛。
5.5 语义检测操作符在长参数上超时
现象:某个接口传了超长参数,WAF 节点 CPU 飙高、请求超时。原因:detect_sqli 对超长输入做词法分析,复杂度随长度上升,没有长度上限保护。解决:在调用语义检测前加长度阈值判断,超过阈值的输入直接按可疑处理或截断检测,避免单请求拖垮节点。
6. 设计取舍:为什么没有一套规则引擎能通吃
把这几层拆完,会发现 WAF 的每个设计决策都是在做取舍,没有银弹。流量接入层,DNS 调度实现简单、成本低,但切换慢;Anycast 切换快、调度灵活,但要整段 IP、路由优化成本高。代理层,Nginx 性能好、生态成熟,OpenResty 在此基础上给了 Lua 扩展能力,代价是脚本层的稳定性和性能需要自己兜底。检测层,正则规则覆盖面广、可解释性强,但对变形攻击天然脆弱;语义分析对变形不敏感,但只覆盖特定攻击类别;自主学习能防未知威胁,但误报和调优成本高。
这些取舍背后有一条共同的判断标准:你的威胁模型是什么。如果面对的是自动化扫描器和已知漏洞利用,CRS 加语义检测的组合性价比最高;如果业务参数结构稳定、担心未知攻击,自主学习的白名单模型值得投入;如果流量规模大、对可用性要求极高,接入层的调度能力比检测层的规则数量更值得关注。
验证一套 WAF 是否真的有效,不能只看它拦了多少条规则命中,要做的是构造变形 payload 做绕过测试——把已知的攻击样本做编码、拆解、大小写变换,看检测是否依然成立。我一般会拿一组标准 payload 和它们的变形版本分别打一遍,对比命中率差异,差异大的地方就是转换链或语义检测的薄弱点。
从那以后我每次评估一套规则引擎,都强制先跑一遍变形 payload 的绕过测试,而不是只看规则条数和覆盖清单。规则多不等于防得住,能扛住变形才是真本事。希望帮到你。
本文还有配套的精品资源,点击获取