OpenResty自建WAF:ModSecurity CRS反向代理与排障

发布时间:2026/10/30 2:55:40
OpenResty自建WAF:ModSecurity CRS反向代理与排障 简介这份PPT资源面向Web安全初学者、运维与安全工程师以及需要选型WAF的技术负责人系统梳理WAF从产品形态到架构落地的完整脉络帮助读者理解如何用七层HTTP(S)流量检测阻断SQL注入、XSS、命令注入、文件包含、CSRF、恶意爬虫与CC等攻击。资源包共1个文件为pptx演示文稿整体约3.93MB以图文对比方式呈现硬件盒子、软件WAF与云WAF三类形态及代表厂商讲解DNS调度与Anycast两种流量接入方式、Nginx与OpenResty反向代理选型以及ModSecurity CRS与Naxsi规则引擎的差异和Lua扩展思路。内容还细化了规则配置中的PASS、DENY、ALLOW、REDIRECT等动作vars参数组与匹配条件的写法以及各执行阶段的功能划分。已有315人学习适合用来搭建WAF知识框架、做技术选型参考或团队内部分享。1. 从一台被扫穿的测试机说起WAF 该装在哪一层一台刚放公网的测试机上线第三天日志里攒了几万条 404路径全是/wp-login.php、/.env、/actuator/env这类。应用本身没漏洞但这类探测只要有一条命中老组件后面就是数据被整库拖走。WAF 干的就是这件事对七层 HTTP(S) 流量做检测把 SQL 注入、XSS、网页木马上传、命令代码注入、文件包含、敏感文件访问、第三方组件漏洞攻击、CSRF、恶意爬虫扫描和 CC 挡在应用前面。知道它能干什么不难难的是装在哪一层。同一套 WAF装成硬件盒子串行旁路、装成 Web 服务器上的一个模块、装成 CDN 后面的反向代理配置方式完全不是一回事。这篇按第三条路走自有服务器上用 OpenResty 做反向代理前面挂规则引擎从依赖、编译、规则写法一路记到误杀排查和上线验证。适合手上有自己机器、愿意动手编译又不想把全部流量和规则细节交给外部服务的开发者。2. 选型与依赖OpenResty 反向代理 ModSecurity CRS 这套组合怎么定版本2.1 三种 WAF 形态自建时只能落到软件这一种硬件形态是独立盒子串行或旁路部署在本地网络也有把防护能力集成进下一代防火墙的产品线软件形态直接装在已有的 Web 服务器上以 ModSecurity、Naxsi 这类开源软件为代表云形态多由 CDN 发展而来在加速能力之上叠一层防护本质是浏览器和真实服务器之间的中间人靠反向代理执行安全控制。三种形态的检测思路大同小异差别在流量怎么进门。对自建的人来说能自己动手的只有软件形态。硬件要采购和上架流量接入得改网络拓扑云形态要改 DNS 解析或加 CNAME规则细节多半只能调到一个开关粒度。软件形态的好处是所有东西都在自己手里——规则文件、日志格式、超时、限速阈值、性能参数坏了能进去看。坏处也同样直接编译、调优、误杀处理全得自己扛没有别人替你兜底。我一般把自建的边界划得很清楚反向代理这一层负责接入、还原真实 IP、做连接层限速规则引擎这一层负责内容检测和阻断应用层只负责业务。分层之后出问题是哪一层看日志位置就能判断不用在一堆配置里猜。2.2 反向代理层为什么选 OpenResty 而不是原生 Nginx原生 Nginx 做反向代理足够稳静态文件、缓存、SSL 卸载也没问题但它没有内置的脚本能力想在请求链路上做点自定义检测要么走 C 模块要么外挂一个进程改一次编译一次。OpenResty 是在 Nginx 之上打包了 LuaJIT 和一组 Lua 库的发行版请求的每个阶段都留了 Lua 入口规则改完 reload 就生效不用重新编译。具体到 WAF 这个场景OpenResty 的几项能力是刚需能力原生 NginxOpenResty请求阶段介入需写 C 模块每个 phase 都有 Lua 钩子规则热更新重新编译改 Lua 文件后 reload检测逻辑扩展受限可调用 Lua 生态库四层/七层代理支持支持缓存与静态服务支持支持另有一类以 ACL 见长的代理原生 ACL 能组合出很复杂的转发规则转发性能比 Nginx 略好官方也自带后端健康检查纯做四层转发时很舒服。但它的定位是转发想在请求体里做内容匹配就得另想办法规则引擎嵌不进去所以这套方案里我不用它做 WAF 入口。OpenResty 里的阶段钩子是整套方案的骨架记住几个关键的就行ssl_certificate_by_lua处理证书set_by_lua做变量初始化rewrite_by_lua做转发、重定向、缓存access_by_lua做请求头处理和检测content_by_lua生成内容header_filter_by_lua和body_filter_by_lua处理响应头和响应体log_by_lua在会话结束后异步写日志。WAF 的主战场是access_by_lua因为它在反向代理之前执行拦得住才有意义。2.3 规则引擎CRS 的正则匹配与 Naxsi 的自主学习怎么选规则引擎有两条主流路线。一条是 ModSecurity 加 ModSecurity CRS 核心规则集逻辑是规则验证本质是正则匹配覆盖面很全通用 Web 十大风险清单OWASP Top10里的常见威胁基本都有对应规则CRS 精简版大约 257 条。它对已知漏洞非常有效代价是规则库重误杀要靠调阈值和加白名单。CRS 里的语义分析不是纯正则它靠 libinjection 和 libhtmlentites 两个库分别做 SQL 注入和 XSS 的语义识别这部分比手写正则准得多。另一条是 Naxsi思路完全不同不依赖规则库先跑一段学习期自主学习正常请求的形态再据此生成规则所以对未知威胁有一定防御力。代价是学习期必须认真养样本不够就会漏样本太杂就会误杀上线节奏比 CRS 慢。我的做法是两个都不放弃CRS 先开检测模式跑一周看日志里哪些规则在误报Naxsi 在流量稳定的业务上做补充主站还是以 CRS 为主。规则引擎最怕的不是漏报是上线第一天把正常下单请求全拦了业务方直接找过来那时候再回滚信任已经没了。2.4 依赖清单与内核参数configure 之前先过一遍编译 OpenResty 和 ModSecurity 会在同一个地方卡住——缺开发包。下面这张表是我在干净系统上必查的项缺哪个后面编译就会在哪一步停依赖用途缺失时的表现gcc / gcc-c / make编译工具链configure 直接报 no acceptable C compilerpcre-devel正则支持报 the HTTP rewrite module requires PCREzlib-develgzip 压缩报 zlib library not foundopenssl-develHTTPS报 SSL modules require OpenSSLlibxml2-develModSecurity 解析 XML 配置编译 libmodsecurity 时报错libcurl-develModSecurity 部分功能链接阶段 undefined referencelibyajl-develJSON 解析同上libgeoip-devel地理信息变量规则里用 GEO 变量时失效perl / readline-devel构建脚本make 阶段奇怪地中断一条命令装齐# 干净机器上一次装完省得 configure 反复中断 yum install -y gcc gcc-c make perl pcre-devel zlib-devel openssl-devel \ libxml2-devel libcurl-devel libyajl-devel libgeoip-devel \ readline-devel autoconf automake libtool git用 Debian 系的话把包名换掉pcre-devel对应libpcre3-devlibxml2-devel对应libxml2-dev其余同理。装完执行pcre-config --version确认一下版本太老会在 configure 阶段被拒绝。系统参数这块WAF 是流量入口连接数比后端应用高一个量级。三处要改# 文件描述符上限worker_connections 乘 worker 数不能超过它 echo * soft nofile 65535 /etc/security/limits.conf echo * hard nofile 65535 /etc/security/limits.conf # 半连接队列突发流量进来时 SYN 不会丢 sysctl -w net.core.somaxconn32768 sysctl -w net.ipv4.tcp_max_syn_backlog16384nofile是硬限制worker_connections一旦配得比它大Nginx 启动时会报worker_connections exceed open file resource limit日志里只给一行警告很容易被忽略。somaxconn改完记得写进/etc/sysctl.conf否则重启就回去了。另外确认 SELinux 状态getenforce返回Enforcing时反代转发到本机端口会被拦表现是 502 但后端明明活着这是常被当成玄学问题的一类。3. 编译安装 OpenResty从源码到能扛住第一个请求的反向代理3.1 源码获取与目录规划源码从官方仓库取稳定分支不要用 master。目录规划上我固定用/usr/local/openresty做前缀配置文件和规则文件单独放在/usr/local/openresty/nginx/conf/waf/下升级时只覆盖二进制和内置 lualib自建配置不受影响。cd /usr/local/src # 取稳定分支master 的 Lua API 偶尔会变 git clone --depth 1 -b v1.21.4.3 openresty 官方仓库 openresty cd openresty # 编译前先看依赖是否被识别这一步比 make 失败后回看日志省事 ./configure --help | grep -E luajit|realip|stub_status--depth 1只拉最近一次提交源码包很大历史记录对编译没意义。版本号按仓库当前的稳定分支来不要死记git tag里以v开头、不带rc的就是稳定版。3.2 configure 与 make参数怎么取舍./configure \ --prefix/usr/local/openresty \ --with-luajit \ --with-pcre-jit \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-http_auth_request_module \ --with-threads \ --with-file-aio make -j$(nproc) make install逐项说一下为什么留。--with-luajit是整套方案的前提不加这个后面的 Lua 钩子全是空的--with-pcre-jit让正则编译结果缓存下来规则引擎跑几千条正则时差距明显--with-http_realip_module用于还原真实 IP没有它所有请求的来源都是上一层代理的地址风控和限速全废--with-http_stub_status_module只为了一个/nginx_status接口看连接数排查时很省事--with-threads和--with-file-aio在有大文件上传下载的业务里影响吞吐。-j$(nproc)只加在 make 上configure 不接受这个参数写错了会直接报未知选项。机器内存小于 2G 时把并发降到-j2否则编译 LuaJIT 那段容易被 OOM killer 干掉表现是 make 突然Killed日志里什么都没有。装完先验证二进制位置这是第一道分水岭/usr/local/openresty/nginx/sbin/nginx -V # 输出里必须能看到 --with-http_realip_module 和 luajit 相关信息 which nginx # 如果这里返回 /usr/sbin/nginx说明系统里还有另一个 nginx后面所有 reload 都可能改错文件which nginx指向系统自带的那个是后面一连串诡异问题的源头后面避坑那章会细说。3.3 nginx.conf 最小可用配置与真实 IP 还原worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 10240; use epoll; multi_accept on; } http { # 自建 lualib 放在前面保证自写模块优先于内置同名模块 lua_package_path /usr/local/openresty/nginx/conf/waf/?.lua;;; include mime.types; default_type application/octet-stream; log_format waf $remote_addr - $http_x_forwarded_for $request $status $body_bytes_sent $http_user_agent $http_referer; access_log logs/access.log waf; # 上一层代理和 SLB 的网段都写进来否则 remote_addr 永远是代理 IP set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; real_ip_header X-Forwarded-For; real_ip_recursive on; client_max_body_size 20m; client_body_buffer_size 128k; proxy_http_version 1.1; proxy_set_header Connection ; upstream backend { server 127.0.0.1:8080; keepalive 64; } server { listen 80; server_name _; location / { # 检测放在 access 阶段在反向代理之前执行 access_by_lua_file /usr/local/openresty/nginx/conf/waf/access.lua; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://backend; } location /nginx_status { stub_status; allow 127.0.0.1; deny all; } } }几个参数值得单独讲。real_ip_recursive on表示从右往左逐层剥离可信代理找到第一个不可信地址作为真实客户端不开这个攻击者自己伪造一个X-Forwarded-For就能把来源改成任意 IP所有基于 IP 的白名单直接失效。client_max_body_size默认只有 1M业务有文件上传时不改用户会看到 413而不是被 WAF 拦排查方向容易跑偏。access_by_lua_file和proxy_pass可以共存因为分属不同阶段换成content_by_lua_file就不行了content 阶段会接管响应生成proxy_pass直接被忽略请求打不到后端表现是 404 或空响应这个坑后面还会提到。配置写完先做语法检查再启动别直接 reload/usr/local/openresty/nginx/sbin/nginx -t # 输出 syntax is ok / test is successful 才算过 /usr/local/openresty/nginx/sbin/nginx curl -I http://127.0.0.1/nginx_status-t只校验语法不校验 Lua 文件里的运行时错误access.lua有语法问题时-t是过的第一个请求进来才 500。所以启动后必须实际打一个请求这一步不能省。3.4 用 systemd 托管并做第一次 reload手工敲命令启动只适合第一次验证正式跑要交给系统管# /etc/systemd/system/openresty.service [Unit] DescriptionOpenResty with WAF Afternetwork.target [Service] Typeforking PIDFile/usr/local/openresty/nginx/logs/nginx.pid ExecStartPre/usr/local/openresty/nginx/sbin/nginx -t ExecStart/usr/local/openresty/nginx/sbin/nginx ExecReload/usr/local/openresty/nginx/sbin/nginx -s reload ExecStop/usr/local/openresty/nginx/sbin/nginx -s quit Restarton-failure LimitNOFILE65535 [Install] WantedBymulti-user.target关键是ExecStartPre里那条-t配置写坏了服务起不来而不是把线上打挂。ExecStop用quit不用stopquit是优雅退出等在处理的请求结束stop是快速退出正在上传大文件的用户会直接断连。systemctl daemon-reload systemctl enable --now openresty systemctl reload openresty # 改完规则用这个不断连接reload 的原理是 master 进程重新读配置、起新 worker、给旧 worker 发退出信号旧 worker 处理完存量请求后自然退出。所以规则文件改错时 reload 不一定报错——Lua 文件是运行时加载的reload 只重新读 nginx.conf。这一点在改规则那章会再强调一次。4. 接入规则引擎ModSecurity CRS 与 Lua 规则表的两种写法4.1 ModSecurity v3 的编译与连接器挂载ModSecurity 有 v2 和 v3 两个分支v2 是 Apache 的模块v3 拆成了独立库 libmodsecurity通过连接器挂到 Nginx 上。要跑在反向代理前面必须用 v3。cd /usr/local/src # v3 分支依赖比 v2 多libxml2、libcurl、libyajl、libgeoip 一个都不能少 git clone --depth 1 -b v3/master modsecurity 官方仓库 modsecurity cd modsecurity ./build.sh ./configure --prefix/usr/local/modsecurity --disable-examples make -j$(nproc) make install # 让系统能定位到库否则连接器编译时找不到 echo /usr/local/modsecurity/lib /etc/ld.so.conf.d/modsecurity.conf ldconfig--disable-examples跳掉示例程序编译快不少也避免示例里的路径在非标准目录下报错。装完执行modsec-rules-check或者直接看/usr/local/modsecurity/bin下有没有生成的可执行文件确认装成功了。连接器要挂进 Nginx这是个容易出错的地方连接器是和某个 Nginx 版本绑定编译的OpenResty 内置的 Nginx 和公版 Nginx 在模块 ABI 上并不完全一致。常见做法有两个——一是用公版 Nginx 编译连接器专门做规则检测层OpenResty 放在它后面做代理和限速两层分开部署二是在 OpenResty 源码目录里替换掉 Nginx 部分再编连接器稳定性看具体版本组合我自己踩过几次之后就不折腾了直接选第一种。# 公版 Nginx 这一层的配置片段 http { modsecurity on; modsecurity_rules_file /usr/local/nginx/conf/modsecurity/main.conf; ... }modsecurity on是总开关关掉后规则文件里的内容全部跳过只保留日志埋点modsecurity_rules_file指向主配置规则通过Include一层层挂进来。4.2 CRS 规则集的加载与裁剪主配置文件写清楚引擎模式、请求体处理和规则挂载位置# /usr/local/nginx/conf/modsecurity/main.conf # 上线前一律 DetectionOnly观察期过了再切 On SecRuleEngine DetectionOnly SecRequestBodyAccess On SecRequestBodyLimit 13107200 SecRequestBodyNoFilesLimit 131072 SecResponseBodyAccess Off SecDebugLog /var/log/modsecurity/debug.log SecDebugLogLevel 0 SecAuditEngine RelevantOnly SecAuditLog /var/log/modsecurity/audit.log Include /usr/local/modsecurity-crs/crs-setup.conf Include /usr/local/modsecurity-crs/rules/*.confSecRuleEngine三个取值要背下来On拦截DetectionOnly只记日志不拦Off全关。SecRequestBodyAccess On是检测 POST 参数的前提关掉之后请求体里的注入一律看不见这是漏报最常见的配置原因。SecResponseBodyAccess Off是因为响应体检测吃内存吃 CPU收益不高业务出现信息泄露再单独开。CRS 默认的拦截策略是基于异常分数累计的命中一条规则加几分累计到阈值才拦所以要调的是分数而不是单条规则# 阈值和敏感级别级别越高规则越严误杀也越多 SecAction id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level1 SecAction id:900110,phase:1,nolog,pass,t:none,setvar:tx.inbound_anomaly_score_threshold5 SecAction id:900300,phase:1,nolog,pass,t:none,setvar:tx.outbound_anomaly_score_threshold4paranoia_level从 1 开始1 只跑基础规则主要覆盖 SQL 注入、XSS、命令注入这类硬攻击调到 3 或 4 会带上更严格的协议校验和编码校验误报率跟着涨。业务后台有富文本编辑器、有大量带特殊字符的搜索参数时直接上 3 基本活不过一天。inbound_anomaly_score_threshold设成 5 是入门值观察到误报集中后要么加白名单要么把它调到 7别一上来就关规则。单条规则误杀的排除方式也要提前知道# 按规则 ID 排除适合明确知道是哪条规则误报 SecRuleRemoveById 942100 # 按路径排除管理后台的富文本提交通常整段放行 SecRule REQUEST_URI beginsWith /admin/editor \ id:10000,phase:1,pass,nolog,ctl:ruleRemoveTargetById942100;ARGS:contentSecRuleRemoveById是全站生效的删了就是删了能写成ctl:ruleRemoveTargetById就写成按参数排除范围小得多。第二种写法里的ARGS:content指定只对content这个参数解除该规则其他参数照样检测。4.3 Lua 规则表的字段语义action、vars、operator、transform如果坚持单层 OpenResty 架构走 Lua 规则表这条路把 ModSecurity 那套规则机制用 Lua 重写规则是普通的 Lua table加载速度快改完 reload 就生效。规则的基本结构是动作加匹配条件-- /usr/local/openresty/nginx/conf/waf/rules.lua local _M {} _M.rules { { id 1001, msg 疑似 SQL 注入, action DENY, -- PASS / DENY / ALLOW / REDIRECT / RESET_CONNECTION match { { -- 同一组 vars 内部是「或」的关系多个 match 之间默认「与」 vars { { var ARGS }, -- 全部 GET/POST 参数的值 { var REQUEST_URI }, }, -- 转换函数按数组顺序叠加顺序写反会漏报 transform { uri_decode, lowercase, compress_whitespace }, operator detect_sqli, -- libinjection 语义识别不需要 pattern pattern , op_negated false, -- 取反默认 false }, }, }, { id 1002, msg 疑似 XSS, action DENY, match { { vars { { var HTTP_USER_AGENT }, { var HTTP_REFERER } }, transform { uri_decode, html_decode, lowercase }, operator detect_xss, pattern , }, }, }, { id 1003, msg 敏感路径探测, action DENY, match { { vars { { var URL } }, transform { normalise_path, lowercase }, operator regex, pattern [[\.(env|git|svn|bak)$|/(phpmyadmin|actuator|druid)/]], }, }, }, { id 1004, msg 内网来源放行, action ALLOW, match { { vars { { var REMOTE_ADDR } }, operator ip_utils, pattern { 10.0.0.0/8, 192.168.1.100, 192.168.1.1-192.168.1.50 }, op_negated true, -- 取反不在名单内才继续往下走 }, }, }, } return _M几个字段的语义必须吃准。action决定命中后做什么PASS跳过当前阶段的这条规则继续往下DENY阻断请求ALLOW直接跳过所有检查REDIRECT重定向RESET_CONNECTION重置连接。ALLOW要慎用写错位置等于给整个站点开了后门。vars参数组是取值的来源常用的就那么些REQUEST_METHOD、URL、REQUEST_URI、ARGS、HTTP_USER_AGENT、HTTP_REFERER、REMOTE_ADDR、REQUEST_HEADERS、RESPONSE_HEADERS、TIME_WDAY。parse子字段用来进一步收窄取值范围specific指定取某个 keyignore忽略某个 keykeys取全部键名values取全部值all键和值都取这五个只能出现一种写两个运行时会报错。operator里有两类特别有用。语义类的detect_sqli和detect_xss底层就是 libinjection 和 libhtmlentites比正则准也不容易绕过范围类的ip_utils支持单 IP、子网掩码和-连接的 IP 段num_range支持[10, 13, 32-126]这种写法str_range支持时间区间[01:42:00-04:32:00]做后台登录时段限制很方便。pf是从文件读 pattern和pattern互斥只支持绝对路径规则特别多的时候用它比写在 Lua 表里省内存。transform是最容易写错的部分。常用的有uri_decode、html_decode、base64_decode、sql_hex_decode、hex_decode、lowercase、uppercase、compress_whitespace、remove_comments、remove_nulls、normalise_path、trim系列、length、md5、sha1。顺序很重要先解码再转小写再压缩空格攻击者用%20%20填充的变体才抓得到反过来先压缩空格再解码编码后的空格根本没被识别直接漏过去。做路径类检测时normalise_path必须放前面否则//admin/../admin这类变形过不了正则。4.4 两种写法的取舍CRS 那套的优势是规则成熟、覆盖面广、更新可跟踪适合主站和对外业务代价是规则重、误报要养、和 OpenResty 的集成要多一层进程或一次重编译。Lua 规则表的优势是轻、快、完全可控写什么测什么适合把业务特有的逻辑补进去比如某接口的参数必须全数字、某路径只允许内网访问代价是得自己维护规则质量覆盖率取决于投入。我的常用组合是公版 Nginx 加 ModSecurity 挡在前面覆盖通用攻击OpenResty 在后面做接入、限速和业务级 Lua 规则两层日志分开记。这样任何一层出问题另一层还在回滚也只回一层。5. 排查与避坑上线前后最容易翻车的五个细节5.1 安装与启动阶段的三个坑现象nginx -t通过启动后请求返回 404后端日志里一条记录都没有。原因location里用了content_by_lua_file同时写了proxy_pass。content 阶段会接管响应生成反向代理配置被静默忽略请求根本没转发出去。 解决检测逻辑挪到access_by_lua_fileproxy_pass保留。如果确实需要 content 阶段生成响应就别配proxy_pass二选一。现象编译连接器时一路报undefined reference to ...或者模块加载时提示 API 版本不匹配。原因连接器是跟着特定 Nginx 源码树编译的OpenResty 内置的 Nginx 打了补丁模块 ABI 和公版不一致。 解决不硬凑拆成两层——公版 Nginx 编译连接器做规则检测OpenResty 做前置代理。真要单层就用--with-compat加对齐版本号重编但每次升级 OpenResty 都得重来一次。现象systemctl reload openresty之后规则变了但没生效攻击照样穿过去。原因reload 只重新读nginx.confaccess_by_lua_file引用的 Lua 文件是运行时require的LuaJIT 有缓存同进程内不会重新加载。 解决改完规则确认两件事一是文件内容真的落盘了二是进程是否真的重启过 worker。生产上我宁可systemctl restart一次用几秒抖动换确定性也不赌缓存失效的时机。5.2 规则与拦截阶段的三个坑现象WAF 日志里攻击记录一堆实际业务一点没受影响以为在拦其实全是放行。原因SecRuleEngine DetectionOnly从没改过。观察期配置写进模板后一路复制切上线忘了改这一行。 解决把这一行写进上线检查清单切换时grep -n SecRuleEngine main.conf确认是On然后用一条构造好的注入请求实测看返回 403 还是 200。现象开启拦截后大量正常请求 403日志指向 CRS 的某条规则但请求参数里看不出任何攻击特征。原因业务参数里带select、union、order by这类词或者富文本提交的 HTML 被当成 XSS。paranoia_level越高越容易触发。 解决先用SecRuleRemoveTargetById按具体参数排除别全站删规则确认误报集中后把tx.inbound_anomaly_score_threshold从 5 调到 7减少单条低分规则凑够阈值的情况。调完必须回头看一周日志确认不是把真攻击一起放过去了。现象所有请求的来源 IP 都是同一个地址基于 IP 的限速和黑名单完全不起作用。原因set_real_ip_from没写全或者上游代理传的 header 名不是X-Forwarded-For。real_ip_recursive关闭时攻击者伪造 header 还能把自己改成任意 IP。 解决把上游所有代理网段写进set_real_ip_fromreal_ip_header确认与上游一致real_ip_recursive on打开。改完在日志里比对$remote_addr和$http_x_forwarded_for两边对得上才算配置生效。现象POST 接口的注入怎么都拦不住GET 参数却正常。原因SecRequestBodyAccess Off或者 Lua 规则里只取了ARGS的 URI 参数没有读请求体。请求体没解析检测逻辑看到的是空字符串。 解决确认SecRequestBodyAccess On且client_body_buffer_size足够放下请求体Lua 规则里在读参数前先调ngx.req.read_body()再取ngx.req.get_post_args()。6. 装完怎么验证三条流量把拦截链路跑通装完不验证等于没装。我的验证顺序是固定三条流量从放行到拦截走一遍任何一条不符合预期就停下排查不往下走。验证项命令期望结果正常请求透传curl -I http://127.0.0.1/health200后端日志有记录真实 IP 还原curl -I -H X-Forwarded-For: 1.2.3.4 http://127.0.0.1/healthaccess.log 首列是 1.2.3.4SQL 注入拦截带union select的查询参数403WAF 日志有命中记录XSS 拦截带scriptalert(1)/script的参数403且是被 XSS 规则命中敏感路径访问/.env403 或 404不能返回文件内容前两条测的是「该放的放过去」后三条测的是「该拦的拦住」。第三条要给个具体写法注意参数要 URL 编码否则 shell 会先处理掉特殊字符# 构造一条明显的注入请求看是否被 403 curl -i http://127.0.0.1/search?q1%27%20union%20select%201,2,3-- # 对同一路径换成正常参数做对照排除是路由本身返回 403 curl -i http://127.0.0.1/search?qhello两条结果不一致说明拦截链路是通的两条都是 403说明拦截位置搞错了很可能是路由权限而不是 WAF两条都是 200回去看SecRuleEngine和规则文件有没有真正加载。再补一个很多人会漏的检查——确认检测发生在反向代理之前# 后端服务关掉请求如果被 WAF 拦住说明检测在代理之前 systemctl stop backend curl -i http://127.0.0.1/search?q1%27%20union%20select%201,2,3-- # 期望 403 而不是 502返回 403说明 WAF 在请求到达后端之前就拦下了这才是入口防护该有的位置返回 502说明检测逻辑跑在代理之后后端一挂 WAF 就形同虚设这种部署顺序得改。最后看日志格式对不对日志字段缺失比规则失效更难发现tail -n 5 /usr/local/openresty/nginx/logs/access.log # 确认这一行里有真实 IP、UA、Referer 三个字段缺一个后面排查就少一条线索从那以后我每次上线 WAF 都强制走一遍这套验证哪怕只是改了一条规则也要重新打这三条流量。规则文件是可以改的验证流程不能省翻车基本都是省掉这一步省出来的。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询