简介:这份PPT面向Web安全初学者与运维开发人员,系统梳理WAF从产品形态到架构落地的完整知识脉络,帮助读者理解如何为Web应用构建七层攻击防护。内容覆盖SQL注入、XSS、CSRF、恶意爬虫与CC攻击等常见威胁的识别与阻断思路,并对比硬件盒子、软件WAF与云WAF三类产品形态及绿盟、启明星辰、安全狗、CloudFlare等代表厂商。架构部分重点讲解云WAF的流量接入方式(DNS调度、CNAME、Anycast)、HTTP(S)反向代理选型(Nginx与OpenResty的Lua扩展阶段)以及安全规则引擎的取舍,深入剖析ModSecurity配合CRS规则集与Naxsi自主学习两种路线,并给出action、rule、vars等配置要素的说明。资源包仅含1个pptx文件,约3.93MB,页面以架构图与要点对照呈现,便于直接用于内部分享或自学梳理。目前已有314人学习,适合希望快速建立WAF全局认知并指导选型实践的读者。
1. 从一次误报说起:WAF 方案到底要解决什么
某次内部演练,一个正常的搜索请求被拦了,返回 403。排查半天发现是规则里detect_sqli把带单引号的关键词当成了注入。这件事说明一个常被忽略的事实:WAF 的难点从来不是"能不能拦",而是"拦得准、拦得住、拦了还能查"。这份《WAF 的实现与架构演进》方案要解决的正是这个问题——从流量怎么接进来、反向代理怎么选、规则引擎怎么配,到规则怎么写、误报怎么压,给出一条能落地的完整链路。它适合两类人:正在做安全方向毕设或课程设计、需要一套完整架构与配置示例的学生;以及要给业务侧交付一套自建 WAF 方案、需要参考选型与验收口径的工程师。下面按"需求 → 架构 → 实现 → 避坑 → 验收"的顺序展开,中间给足可抄的配置和规则写法。
2. 总体架构设计:流量接入、反向代理与规则引擎的三段式拆分
一套 WAF 无论产品形态是硬件盒子、装在 Web 服务器上的软件,还是云化部署,内部都可以拆成三段:流量接入层负责把用户请求引到 WAF 上;反向代理层负责解析 HTTP(S)、做连接管理和转发;规则引擎层负责判定请求是放行还是阻断。三段职责清晰,替换其中一段不影响另外两段,这是整套方案能演进的基础。
2.1 三种产品形态与它们的适用边界
硬件 WAF 是独立设备,串行或旁路部署在本地网络,传统安全厂商多把它集成进 NGFW 里。它的优势是性能确定、不占业务服务器资源,缺点是扩容要买盒子,弹性差。软件 WAF 直接装在已有 Web 服务器上,以 ModSecurity、Naxsi 这类开源软件为代表,也有商业软件形态,部署成本低,但和业务争抢 CPU 与内存,规则一多就容易拖慢主站。云 WAF 多由 CDN 演进而来,在加速能力之上叠加防护,作为反向代理站在浏览器和真实服务器之间,扩容几乎无感,代价是流量必须经过第三方节点。
选型时我一般按三个问题过一遍:业务是否已经上了 CDN(上了就优先云 WAF,接入成本最低);是否有等保或数据不出机房的硬约束(有就软 WAF 或硬件);峰值 QPS 和可接受的延迟增量是多少(延迟敏感型业务慎用多级代理)。这三个问题答完,形态基本就定了,不用在参数表上纠结太久。
2.2 流量接入的三种方式与调度策略
流量接入是云 WAF 的第一道坎。正常访问是用户 DNS 解析到源站 IP 直连,接入 WAF 后要在这个链路上插一层。常见做法有三种:
| 接入方式 | 原理 | 生效速度 | 成本 | 适用场景 |
|---|---|---|---|---|
| DNS 记录修改 | 直接把域名 A 记录指向 WAF 节点 | 受 TTL 影响,分钟到小时级 | 低 | 小规模、可停机切换 |
| CNAME 接入 | 域名 CNAME 到 WAF 提供的别名 | 受 TTL 影响 | 低 | 云 WAF 主流方式 |
| Anycast 任播 | 同一 IP 在全球多点宣告,路由层调度 | 路由收敛级,快 | 高,需整段 IP | 大流量、多地域 |
DNS 调度还能做更细的分流:某机房被打爆时把一部分流量分到空闲机房;按用户来源把北方流量引到北方节点、南方引到南方节点;按运营商线路把联通用户导到联通机房。Anycast 的表现形式和 DNS 调度类似,但解析出来的 IP 直接就是边缘节点 IP,路由策略变动生效快、单点故障后流量自动切换,缺点是全球 BGP 路由优化繁琐,而且要占用整个 C 段地址,IP 浪费明显。中小项目通常 CNAME 就够了,Anycast 是流量上量之后才需要考虑的事。
2.3 反向代理选型:Nginx 与 OpenResty 的取舍
反向代理层要在 Nginx 和 OpenResty 之间做选择。Nginx 的性能好、网络模型效率高、能充分利用多核,模块化设计可扩展性强,本身还带基础 Web 服务和缓存功能。OpenResty 在 Nginx 之上集成了 LuaJIT,把 Lua 嵌入到请求处理的各个阶段,规则引擎可以直接用 Lua 写,不用改 C 模块、不用重编译。
这个差别在 WAF 场景里是决定性的。安全规则要频繁调整,如果每条规则都走 C 模块,改一次编译一次,迭代成本高得离谱;用 Lua 写规则,改完 reload 就生效。所以自建 WAF 我一般直接选 OpenResty,Nginx 原生 ACL 虽然强大、转发性能也略好,但规则迭代的灵活性跟不上。开源社区里有基于 OpenResty 的 WAF 项目,把 ModSecurity 的规则机制用 Lua 重新实现,思路值得借鉴:代理层用 OpenResty,规则层用 Lua 复刻 ModSecurity 的语义。
3. 规则引擎实现:从阶段挂载到规则 DSL 的完整落地
规则引擎是 WAF 的大脑,决定一个请求是 PASS、DENY、ALLOW 还是 REDIRECT。这一章把 OpenResty 的执行阶段、规则的数据结构、匹配条件的写法、转换函数和操作函数的组合讲透,给出可以直接改参数使用的配置示例。
3.1 OpenResty 执行阶段与检测点的对应关系
OpenResty 把一次请求拆成多个阶段,每个阶段可以挂 Lua 代码,WAF 的检测点就分布在这些阶段上:
ssl_certificate_by_lua:SSL 证书处理,可做 SNI 级别的策略set_by_lua:变量初始化rewrite_by_lua:转发、重定向、缓存,适合做 URL 层面的初筛access_by_lua:请求头处理,这是 WAF 做请求检测的主战场content_by_lua:内容生成header_filter_by_lua:响应头过滤body_filter_by_lua:响应体过滤,防敏感信息泄露、防网页木马回显log_by_lua:会话结束后异步写日志,不阻塞响应
请求侧的 SQL 注入、XSS、命令注入检测放在access_by_lua,因为此时请求头和请求体都已就绪、还没转发到后端,拦下来代价最小。响应侧的敏感数据检测放在body_filter_by_lua,日志落盘放在log_by_lua异步做。这个分工要一开始就定好,否则规则散落在各阶段,后面排查会非常痛苦。
3.2 规则的数据结构:动作、匹配条件与参数组
一条规则由动作和匹配条件两部分组成。动作决定命中后干什么:
-- 动作枚举,对应规则里的 action 字段 local ACTIONS = { PASS = "pass", -- 跳过当前 phase 的后续规则 DENY = "deny", -- 阻断请求,返回 403 ALLOW = "allow", -- 跳过所有检查,直接放行 REDIRECT = "redirect", -- 重定向到指定地址 RESET_CONNECTION = "reset_connection" -- 重置 TCP 连接 }匹配条件是一个数组,数组内多个条件之间是"或"的关系,任一命中即触发动作。每个条件包含变量、解析方式、转换函数、操作函数和操作参数:
-- 单条匹配条件的结构 local match_info = { vars = { -- 变量数组,多个变量之间是"或" { var = "ARGS", -- 参数组名称 parse = { -- 对变量的解析,以下只能出现一种 specific = "id", -- 只取名为 id 的参数 -- ignore = "csrf_token", -- 或忽略某个 key -- keys = true, -- 或取出所有 key -- values = true, -- 或取出所有 value -- all = true -- 或 key 和 value 都取 } } }, transform = { "uri_decode", "lowercase" }, -- 转换函数,可多选,按顺序执行 operator = "detect_sqli", -- 操作函数 pattern = nil, -- 操作参数,detect_sqli 不需要 op_negated = false -- 是否对结果取反 }parse的几种写法互斥,选错会直接导致规则不生效:想检测某个具体参数用specific,想跳过白名单参数用ignore,想全量扫描用all。参数组覆盖REQUEST_METHOD、URL、REQUEST_URI、ARGS、HTTP_USER_AGENT、HTTP_REFERER、REMOTE_ADDR、REQUEST_HEADERS、RESPONSE_HEADERS、TIME_WDAY等,基本覆盖了请求上下文里所有能拿到的信息。
3.3 操作函数与转换函数的组合用法
操作函数决定"怎么比",转换函数决定"比之前先把数据变成什么样"。常用的操作函数包括begins_with、contains、contains_word、ends_with、equal、greater、less、regex、detect_sqli、detect_xss、validate_url_encoding、num_range、str_range、ip_utils。其中detect_sqli和detect_xss走的是语义分析,底层分别依赖 libinjection 和 libhtmlentities 两个库,比纯正则准得多;pf从文件加载 pattern,和pattern互斥,只支持绝对路径。
转换函数是压误报的关键。比如检测命令注入时,攻击者常把cat /etc/passwd写成c'a't /etc/passwd或插入多余空格,先过一遍remove_comments_char去掉/* */、--、#注释符,再过compress_whitespace压缩空格,最后才做匹配,命中率会明显提升。下面是一条检测参数中 SQL 注入的完整规则:
-- 检测 GET/POST 参数中的 SQL 注入,先解码再语义识别 local rule_sqli = { id = 1001, phase = "access", action = "deny", match = { { vars = { { var = "ARGS", parse = { all = true } }, { var = "REQUEST_URI", parse = { values = true } } }, transform = { "uri_decode", "remove_nulls", "compress_whitespace" }, operator = "detect_sqli", op_negated = false } } }uri_decode放在最前面,因为攻击载荷经常是 URL 编码过的,不解码直接匹配等于漏检;remove_nulls处理%00截断绕过;compress_whitespace把多空格压成一个,防止用空格拆分关键字绕过正则。这三个转换函数的顺序不能乱,解码必须在清洗之前,否则清洗的是编码后的字符串,等于白做。
3.4 规则集选型:ModSecurity CRS 与 Naxsi 的差异
规则集有两条路线。ModSecurity CRS 是规则验证路线,核心是正则,规则全面,覆盖 OWASP Top10 常见威胁,CRS 核心规则集约 257 条,配合 ModSecurity 引擎使用,生态成熟、文档多,缺点是正则匹配对性能有消耗,且规则越全误报越多,需要持续调优。Naxsi 走的是另一条路:不依赖规则库,通过自主学习正常流量生成白名单规则,偏离白名单的请求才拦,理论上能防未知威胁,但学习期需要干净流量,学习期混入攻击就会把攻击特征写进白名单。
我的做法是两者结合:用 CRS 打底覆盖已知威胁,用 Naxsi 的白名单思路给核心业务接口做一层收紧。如果项目周期紧、只选一个,选 CRS,因为它的规则可以直接拿来用,Naxsi 的学习期在毕设或短期交付里很难跑出稳定结果。把 ModSecurity 的规则机制用 Lua 在 OpenResty 里复刻,是社区里已经验证过的路径,规则文件格式基本兼容,迁移成本可控。
4. 避坑与排查:五条最容易翻车的实战经验
规则配完不等于能上线,下面五条是项目里反复踩到的坑,每条按"现象 → 原因 → 解决"写,照着排查能省不少时间。
4.1 规则不生效:先查 phase 和 parse
现象:规则写好了,构造攻击请求打过去,请求正常透传到后端,日志里也没有命中记录。原因通常有两个:一是规则的phase和挂载阶段对不上,比如把请求体检测规则写在了rewrite_by_lua,那时请求体还没读;二是parse写错,specific指定的 key 名和实际参数名大小写不一致,或者该用all的地方用了values,导致只扫了 value 没扫 key。解决:先在规则里加一条PASS动作的调试规则,把vars解析结果打到 error log,确认变量取到了值,再换回真实动作。这个调试习惯能省掉一半的排查时间。
4.2 误报压不住:转换函数顺序错了
现象:正常业务请求被拦,日志显示命中了 SQL 注入规则,但参数里只有一个英文单引号或一个--。原因多半是转换函数没做或顺序不对,原始字符串里的合法字符被detect_sqli误判。解决:在语义检测前加remove_comments_char和compress_whitespace,对已知会带特殊字符的业务参数(如富文本内容、搜索关键词)用parse.ignore排除,或者单独建一条ALLOW规则放在前面。误报治理是个持续过程,上线第一周每天看一遍拦截日志是必须的。
4.3 性能断崖:正则回溯把 worker 拖死
现象:压测时 QPS 上不去,CPU 单核跑满,nginx -T看配置没问题,日志里出现大量慢请求。原因是一条写得很宽的正则,比如.*(select|union).*这种,遇到长参数会触发灾难性回溯,单个请求就能占住一个 worker 几秒。解决:正则尽量锚定开头、避免嵌套量词,能用contains、begins_with这类字符串操作函数就别用regex;对请求体大小设上限,超长 body 直接拒绝检测或截断;用log_by_lua记录每条规则的耗时,找出最慢的几条重点优化。
4.4 日志黑洞:异步写日志丢了关键上下文
现象:出了安全事件要回溯,发现日志里只有时间戳和命中规则 ID,没有原始请求内容,根本没法判断是误报还是真攻击。原因是用log_by_lua异步写日志时,为了性能只记了摘要,请求体、请求头这些关键字段没落盘。解决:日志分两级,摘要日志全量记,详细日志(含请求体前 N 字节)按命中规则采样记;请求体落盘前做脱敏,密码、token 字段替换掉。这个设计要在方案阶段就定,事后补日志等于重新做一遍。
4.5 接入切换翻车:DNS TTL 和回源配置没对齐
现象:CNAME 切到 WAF 后,部分用户访问异常,或者切回源站时流量没完全回来。原因一是 DNS TTL 设得太长,切换后旧解析还在缓存里;二是 WAF 回源地址配错,或者源站没放行 WAF 节点 IP,导致回源被拒。解决:切换前把 TTL 调到 60 秒,观察一个 TTL 周期再正式切;回源配置里显式指定源站 IP 和端口,源站防火墙放行 WAF 回源网段;切换后持续对比 WAF 侧和源站侧的访问日志条数,差值超过阈值就回滚。
5. 验收与答辩:用一套可复现的测试用例证明方案成立
方案写完,怎么证明它是对的?答辩或交付时,评委和甲方看的不是你讲了多少架构图,而是你能不能当场复现一次拦截、一次放行、一次误报修复。我一般准备一套固定的测试用例集,覆盖三类场景:已知攻击、正常业务、边界绕过。
已知攻击用例按 OWASP Top10 组织,每条给一个原始请求和一个预期结果。比如 SQL 注入用id=1' OR '1'='1,预期 DENY;XSS 用q=<script>alert(1)</script>,预期 DENY;命令注入用cmd=cat /etc/passwd,预期 DENY;路径穿越用file=../../etc/passwd,预期 DENY。正常业务用例从真实访问日志里采样,比如带单引号的搜索词、带 HTML 标签的富文本提交,预期 PASS。边界绕过用例专门测转换函数,比如把注入载荷做 URL 编码、大小写混写、插入注释符,预期仍然是 DENY——这一组最能体现规则质量。
测试执行用脚本跑,每条用例记录请求、响应码、命中规则 ID、耗时,输出一张表:
| 用例类型 | 用例数 | 预期拦截 | 实际拦截 | 误报数 | 平均耗时 |
|---|---|---|---|---|---|
| SQL 注入 | 20 | 20 | 20 | 0 | 3ms |
| XSS | 15 | 15 | 15 | 0 | 4ms |
| 命令注入 | 10 | 10 | 10 | 0 | 3ms |
| 正常业务 | 30 | 0 | 0 | 1 | 5ms |
| 编码绕过 | 12 | 12 | 11 | 0 | 6ms |
表里出现误报和漏报不可怕,可怕的是说不清为什么。答辩时被问到"这条为什么漏了",能当场打开日志、定位到是uri_decode没加导致编码载荷没解开,比背十页 PPT 都有说服力。性能验收同理,用压测工具在开启和关闭 WAF 两种状态下各跑一轮,给出 QPS 和 P99 延迟的对比,延迟增量控制在业务可接受范围内就算通过。
从那以后我每次交付 WAF 方案,都强制先跑一遍这套用例集再开口讲架构——因为架构讲得再漂亮,一条漏检就能让整场答辩翻车。希望这套结构和用例能帮到你,拿去改改参数就能用在自己的项目里。
本文还有配套的精品资源,点击获取