ModSecurity + OWASP CRS 自建 WAF 实战:在 Nginx 上挡住 SQL 注入与 XSS,从 DetectionOnly 到正式拦截的完整配置

网站被拖库、被挂马、被刷接口,很多时候不是你的代码写得有多差,而是那些"自动化扫描器"每天都在互联网上无差别地扫。它们用现成的 payload 疯狂试探:?id=1' or '1'='1、<script>alert(1)</script>、../../etc/passwd……绝大多数请求连门槛都过不了,但只要有一个 Web 框架的版本漏洞没打补丁,就足够让它们得手。

WAF(Web 应用防火墙)就是挡在 Web 服务器前面的一道专门过滤器:它在 HTTP 请求到达你的 PHP/Node/Python 应用之前,先用一整套规则去匹配请求里的"攻击特征",可疑的直接拦掉、返回 403,连日志都不给你记一条脏的。这篇文章讲的是用 ModSecurity + OWASP CRS(核心规则集)在 Nginx 上自建 WAF 的完整过程——这是目前最主流、规则最成熟、也最容易在个人服务器上落地的开源方案。

一、WAF 到底挡在哪个位置,和普通防火墙有什么不同

先建立一个正确的模型,否则配置容易走偏。普通防火墙(iptables/nftables/安全组)工作在三层四层,看的是 IP、端口、TCP 标志位,它根本看不懂 HTTP 内容——它只知道"有个包发到 443 端口",至于这个包是一条正常的下单请求还是一条 SQL 注入 payload,它无从判断。

WAF 工作在七层(应用层),它把 HTTP 请求解析成结构化的字段:URI、查询参数、请求体、Cookie、各种 Header,然后拿 OWASP CRS 的上千条规则逐条比对。这些规则是对已知攻击模式的提炼,比如"参数里出现了 union select 加注释符"、"Cookie 里出现了 <script>"——命中就按规则的动作处置(拦截、记录、加威胁分)。

和普通防火墙的关系是叠加而不是替代:三层防火墙依然要做端口收口、限速、封 IP;WAF 在应用层再加一道内容过滤。两者一起用才完整。

二、安装:Nginx 需要 ModSecurity 模块

ModSecurity 老版本(2.x)是 Apache 时代的产物,接口陈旧、性能一般。现在的标准是 libmodsecurity 3(ModSecurity v3)加 ngx_http_modsecurity_module。这里有个绕不开的现实:Nginx 官方包不含这个模块,需要么自己编译,要么装带模块的发行版包。

Debian/Ubuntu 上走官方 apt 源最省事(思路同样适用其他发行版,只是包名不同):

# 安装 libmodsecurity3 和 Nginx 的 ModSecurity 连接器
apt-get update
apt-get install -y libmodsecurity3 libnginx-mod-http-modsecurity

# 确认模块被启用(把 -V 输出按空格拆行后搜 modsec)
nginx -V 2>&1 | tr ' ' '~' | tr '~' '\012' | grep -i modsecurity
# 或者看模块目录
ls /usr/lib/nginx/modules/ | grep modsec

如果发行版仓库里没有带模块的 Nginx 包,就得源码编译:下载 Nginx 源码,加 --add-dynamic-module=../ModSecurity-nginx 重新编译。编译前先装好 libmodsecurity3-dev 与编译工具链。这一步最容易卡在版本不匹配上,建议优先在发行版里找带模块的包,编译留作最后手段。

装好后先别急着上线,ModSecurity 有一个"只记录不拦截"的模式,务必先用它跑一段时间。

三、OWASP CRS:规则集怎么来、怎么挂

ModSecurity 本身只是个引擎,没有规则就是空壳。规则由 OWASP Core Rule Set(CRS)提供,它是社区维护的免费规则集,覆盖 SQL 注入、XSS、LFI/RFI(文件包含)、命令注入、协议违规、扫描器指纹等等,还带一套异常评分机制(Anomaly Scoring)。

CRS 的核心设计值得先理解:它不靠单条规则一票否决,而是给每个命中累加分数。比如一条中度可疑的请求加 3 分,一条高度可疑的加 5 分,累计到某个临界分(默认入站 5 分)才拦截。这样能在误报和防御之间取得平衡,也方便你调参。你会在配置里看到 paranoia_level(偏执等级,1–4,越高越严、误报越多),个人站一般用 1 就够。

# 获取 CRS(放到一个专门目录)
mkdir -p /etc/nginx/modsec/crs
cd /etc/nginx/modsec/crs
# 从 GitHub 拉取稳定版(或用发行版提供的 owasp-modsecurity-crs 包)
# git clone https://github.com/coreruleset/coreruleset.git .

# 关键:把 crs-setup.conf.example 复制成正式配置
cp crs-setup.conf.example /etc/nginx/modsec/crs-setup.conf

# 复制规则主入口示例
cp crs/crs-setup.conf.example 2>/dev/null; true

然后把它们挂到 ModSecurity 的主配置 /etc/nginx/modsec/main.conf 里:

# /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf

# 加载 CRS setup(定义评分阈值、允许的 HTTP 方法等)
Include /etc/nginx/modsec/crs-setup.conf

# 加载全部规则文件(按文件名顺序)
Include /etc/nginx/modsec/crs/rules/*.conf

顺序很重要:setup → 规则文件,因为 setup 里定义了评分阈值、异常分变量,规则文件要用到它们。顺序反了会出现规则找不到变量的报错。

四、modsecurity.conf 里必须改的几个开关

发行版自带的 modsecurity.conf 默认几乎是"安全模式全关"的状态,直接拿来用等于没装。逐项检查这几个:

# 1. 引擎开关:DetectionOnly = 只记录不拦截(首次务必用这个)
SecRuleEngine DetectionOnly
# 观察几天、确认误报可接受后,改成 On 开始真正拦截
# SecRuleEngine On

# 2. 请求体检查(不打开等于不检查 POST,SQL 注入全从这儿进)
SecRequestBodyAccess On

# 3. 响应体检查(可选,检查返回内容有没有泄露敏感信息,内存开销较大)
SecResponseBodyAccess On

# 4. 临时文件目录,请求体超过内存阈值会落到磁盘
SecTmpDir /tmp
SecDataDir /tmp

# 5. 审计日志:记录被拦截的请求详情,排错全靠它
SecAuditLogParts ABCFHZ
SecAuditLog /var/log/modsec_audit.log
SecAuditLogType Serial
SecAuditLogRelevantStatus "^(?:5|4(?!04))"

最后那行 SecAuditLogRelevantStatus 的意思是:只对 4xx(排除 404,因为 404 太常见)和 5xx 的响应记审计日志。这样日志不会被正常流量淹没,你查的时候能直接定位到"被拦的那些请求"。

五、挂到 Nginx:server 或 location 级别启用

在 Nginx 配置里,通过 modsecurity 指令开启,粒度可以到 location。最小可用的写法:

# nginx.conf 顶层加载模块(若是动态模块)
load_module modules/ngx_http_modsecurity_module.so;

http {
    # 全局开启
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    server {
        listen 443 ssl;
        server_name www.example.com;

        location / {
            # 也可以只对某些 location 开启/关闭
            # modsecurity off;   # 例如对上传接口临时关闭
            proxy_pass http://127.0.0.1:8080;
        }
    }
}

改完务必先 nginx -t 检查语法,再 systemctl reload nginx(reload 不中断连接,参见 #1409 那篇)。

有一个非常实用的技巧:对确实需要放行的路径单独关掉。比如后台富文本编辑器提交文章时,正文里合法地包含 <script> 或 HTML 标签,会被 XSS 规则误伤;对 /admin/ 这类已登录才可达的路径,可以临时 modsecurity off,把防线集中在面向公众的接口上。这叫"分级豁免",是生产环境调误报的常规手法。

六、验证:怎么确认它真的在工作

配置完不验证,等于没配。用一条经典的注入 payload 打一下自己:

# 这条 URI 参数里带明显的 SQL 注入特征
curl -s -o /dev/null -w "%{http_code}" \
  "https://www.example.com/?id=1+union+select+1,2,3--"; echo

# 预期在 Engine=On 时返回 403;在 DetectionOnly 时返回 200 但会被记日志

如果返回 403,翻审计日志看命中详情:

tail -50 /var/log/modsec_audit.log

# 关键看这几行:
#   [id "942100"]  -> 规则编号(942xxx = SQL 注入系列)
#   [msg "SQL Injection Attack Detected..."]
#   [data "Matched Data: union select found within ARGS:id"]
#   [severity "CRITICAL"]

看到 [id "942100"] 这类条目,说明 CRS 规则命中了并且处置了,WAF 是活的。反过来,如果打了一堆 payload 什么都没记,八成是 SecRuleEngine 还是默认的 Off,或者 SecRequestBodyAccess 没开导致 POST 体根本没被检查。

七、五个必踩的坑

坑 1:直接用 On 上线,误报把正常用户拦在门外

CRS 是通用规则,你的业务里可能有合法的、形式上像攻击的请求(比如返回 JSON 里含 select 字样、参数里带路径)。第一周一定用 DetectionOnly,翻审计日志统计误报,再决定不放行哪些规则。直接 On,轻则用户抱怨"打不开",重则搜索引擎爬虫被拦、收录暴跌——这是最惨的后果。

坑 2:忘了 RequestsBodyAccess,POST 全裸奔

很多人只测了 GET 的参数注入发现拦得住,就以为万事大吉。实际上电商、登录、评论这些都走 POST,若不开启请求体检查,SQL 注入和 XSS 从 body 进来时 WAF 完全看不到。

坑 3:日志写满了磁盘

审计日志 + 大量扫描器流量,/var/log/modsec_audit.log 涨得比你想象得快。务必配 logrotate,并设置保留份数。SecAuditLogRelevantStatus 排除 404 也能显著减量。

坑 4:把 WAF 当成了唯一防线

WAF 防的是"已知的、有特征的"攻击。一个针对你业务逻辑的、参数完全正常的越权请求(比如把 user_id=1001 改成 1002 去看别人数据),WAF 是认不出来的——它没有越权的特征。WAF 补的是通用攻击面,业务逻辑漏洞还得靠代码里的权限校验。两者是互补的。

坑 5:改了规则不测就 reload 到生产

CRS 规则文件很多是共享的 *.conf,你删除或改写某条规则时,容易破坏规则间的变量依赖,导致整个规则文件加载失败。每次改规则后先 nginx -t,再在测试环境验证,最后才 reload 生产;并且用 Include 顺序控制覆盖关系,不要直接编辑 CRS 原始文件(升级时会被覆盖)。要屏蔽某条规则用 SecRuleRemoveById 942100 写在 setup 之后,而不是去删原始规则。

八、什么时候该上 WAF,什么时候不必

不是每个站都需要 WAF。判断标准其实很简单:

  • 有用户输入进数据库的(表单、评论、搜索、登录)——值得上,SQL 注入和 XSS 是自动扫描器的头号目标。
  • 用现成 CMS(WordPress 等)且插件众多——值得上,插件漏洞是重灾区,WAF 能在补丁出来之前先挡一道。
  • 纯静态站、无任何输入——意义不大,把 Nginx 配置和补丁做好就够,WAF 只会增加复杂度和误报风险。
  • 服务器资源非常紧张(内存 < 512MB)——ModSecurity 解析有内存和 CPU 开销,小内存机器要权衡,或只对关键路径开启。

对多数有交互的个人站来说,ModSecurity + CRS 是一笔划算的交易:一次配置,长期挡住海量无差别扫描。它的成本不是钱,是调误报的耐心——用 DetectionOnly 观察一周,把误报的规则一条条加白名单,之后它就能安静地替你值夜班了。

最后提醒一句:WAF 不是"装了就不用管"的。CRS 规则需要定期更新(git pull 或包升级),ModSecurity 的版本也要跟。一个两年没更新的 WAF,挡不住这两年新出现的攻击模式——安全从来是过程,不是一次性动作。

Last modification:October 7th, 2026 at 12:24 pm

Leave a Comment