iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
自我挑戰組

一鍵完成六套開源防禦系統整合系列 第 13 篇

WAF 第 1 篇 元件與規則引擎

  • 分享至 

  • xImage
  •  

nginx、ModSecurity v3、OWASP CRS 三層各做什麼;容器啟動時設定檔怎麼疊起來;一個請求的「異常分數」怎麼算出來、門檻是多少;Paranoia Level 是什麼。讀完本篇,看到 audit.log 裡的 Inbound Anomaly Score Exceeded (Total Score: 8) 就知道 8 是怎麼來的。

一、三層的關係

https://ithelp.ithome.com.tw/upload/images/20260927/20184261wqFCfUCrR9.png

層 負責 不負責
nginx TCP 連線、HTTP 解析、反向代理、回應碼、access log、CORS 與安全標頭、/healthz(容器健康檢查用) 不理解攻擊。它只知道「引擎說要中斷」就回 403
connector 在 nginx 的 rewrite/access/header filter/body filter/log 各階段呼叫引擎;把 $remote_addr、標頭、本文交給引擎 不做判斷、不含規則
libmodsecurity 規則語言、變數集合(ARGS、REQUEST_HEADERS、RESPONSE_BODY…)、轉換(urlDecode、lowercase…)、運算子(@rx、@pm、@detectSQLi…)、動作(deny、setvar、ctl…)、audit log 的產生 不決定「什麼算攻擊」,那是規則的事。沒有規則時它只是一個很貴的 pass-through
OWASP CRS 偵測規則本體、異常分數計分、Paranoia Level 分級、方法與 Content-Type 白名單、回應外洩檢查 不認識任何特定應用;誤判排除要由使用者自己寫(第 4 篇)

二、容器映像:設定全部從環境變數來

本安裝包沒有自己編譯任何東西,直接用 OWASP 官方的 owasp/modsecurity-crs:nginx。這個映像的設計是:nginx 與 ModSecurity 的設定檔都是模板,容器啟動時用環境變數填值後才寫成真正的設定檔。所以 docker-compose.yml 裡 WAF 服務的設定幾乎全是 environment:,而不是掛一堆 .conf 進去。

環境變數(compose 內) 本安裝包的值 填進哪裡、作用
BACKEND ${WAF_BACKEND_URL} nginx 的 proxy_pass。被保護網站的內網位址
PORT / SSL_PORT 8080 / 8443 nginx 監聽埠。compose 只發布 8080;8443 在容器內存在但外面連不到
MODSEC_RULE_ENGINE ${WAF_RULE_ENGINE},預設 On SecRuleEngine。On 會擋;DetectionOnly 只記錄不擋,全部規則照跑、分數照算、audit.log 照寫,只是不中斷
PARANOIA ${WAF_PARANOIA},預設 1 CRS 的 tx.blocking_paranoia_level,見第七節
ANOMALY_INBOUND / ANOMALY_OUTBOUND 5 / 4 請求與回應的異常分數門檻,見第六節。5 與 4 是 CRS 的出廠預設,本安裝包沒有調
MODSEC_AUDIT_ENGINE RelevantOnly SecAuditEngine。只記「相關」的交易,第 3 篇第一節
MODSEC_AUDIT_LOG / _FORMAT / _PARTS /var/log/modsec/audit.log / JSON / ABCFHJZ audit log 的路徑、格式與內容段落。路徑掛到主機 waf/log/ 給 Vector 讀
SET_REAL_IP_FROM / REAL_IP_HEADER 127.0.0.2 / CF-Connecting-IP nginx real_ip 模組。刻意設成永遠不會匹配的值,理由在第 2 篇第六節
SERVER_NAME _ 接受任何 Host,hostname 的比對交給 cloudflared ingress 做
BLOCKING_LOG_LEVEL / ERRORLOG_LEVEL warn / warn nginx error log 的詳細度。攔截事件會以 warn 進 docker compose logs waf-nginx

除了環境變數,本安裝包只掛了兩個檔案進容器,都是模板:

主機檔案 作用
waf/nginx-templates/logging.conf.template nginx access log 格式,在原廠格式前多記一欄 $realip_remote_addr,方便對照「real_ip 換算前後的來源」。這份 log 沒有任何程式在讀,只是除錯用
waf/modsec-overrides/modsecurity-override.conf 本安裝包對 CRS 的覆寫:方法白名單與路徑層規則排除(第 4 篇第二節)。掛進 /etc/nginx/templates/modsecurity.d/,容器啟動時展開成 /etc/modsecurity.d/modsecurity-override.conf

三、設定檔載入順序

nginx 的 modsecurity.conf 只有兩行:開引擎、指定一個入口檔 /etc/modsecurity.d/setup.conf。所有規則都從這個入口依序 Include 進來,順序決定了誰能覆寫誰。這是映像檔內的實際內容(省略註解):

Include /etc/modsecurity.d/modsecurity.conf              # 1 引擎行為
Include /etc/modsecurity.d/modsecurity-override.conf     # 2 本安裝包的覆寫
Include /etc/modsecurity.d/owasp-crs/crs-setup.conf      # 3 CRS 全域參數
Include /etc/modsecurity.d/owasp-crs/plugins/*-config.conf
Include /etc/modsecurity.d/owasp-crs/plugins/*-before.conf
Include /etc/modsecurity.d/owasp-crs/rules/*.conf        # 4 規則本體(901 → 999)
Include /etc/modsecurity.d/owasp-crs/plugins/*-after.conf
# 檔案 內容
1 modsecurity.conf 引擎層設定:SecRuleEngine、要不要讀請求本文與回應本文、本文大小上限、audit log 條件與格式。與「什麼算攻擊」無關。實際值見第五節
2 modsecurity-override.conf 本安裝包的覆寫。放在 crs-setup 之前是有意的:這裡設的 tx.allowed_methods 這類變數,要趕在 CRS 的 901 初始化規則讀取之前存在,否則會被 CRS 的預設值蓋掉
3 crs-setup.conf CRS 的全域參數:Paranoia Level、異常分數門檻、預設允許的方法與 Content-Type 等。映像檔用環境變數把 PARANOIA、ANOMALY_INBOUND 這些填進去
4 rules/*.conf 規則本體,依檔名數字順序載入:901 初始化 → 905 常見例外 → 911~944 請求檢查 → 949 請求評分 → 950~956 回應檢查 → 959 回應評分 → 980 關聯記錄。數字就是規則 ID 的前三碼,看到 942100 就知道它在 942(SQLi)檔裡
— plugins/* CRS 外掛的掛載點。本安裝包沒裝任何外掛,這幾行實際上是空的

容器啟動時 nginx 會把整套規則載入記憶體(啟動 log 會印 rules loaded inline/local/remote: 0/850/0)。覆寫檔有語法錯誤時容器起不來、網站直接不通,改完覆寫檔一定要看 docker compose logs waf-nginx。

四、關於 CRS 的三個常見誤會

誤會 實際
「命中一條規則就會被擋」 不是。CRS 4 的預設模式是異常分數(anomaly scoring):每條命中規則只加分,最後由 949 規則看總分有沒有到門檻。單一條 WARNING 等級的規則(3 分)不會讓請求被擋。這也是為什麼 audit.log 裡會有「命中規則但回 200」的交易
「WAF 會看懂應用程式邏輯」 不會。CRS 是通用規則,只認字串型態:SQL 語法片段、shell 命令、HTML 標籤、路徑穿越序列。「這個帳號沒權限看那筆資料」這種邏輯漏洞它完全看不到
「規則會自己更新」 不會。規則跟映像檔綁在一起,拉到什麼版本就用到什麼版本。Suricata 有 suricata-update 每日抓規則,WAF 沒有對應機制,要更新得重拉映像(第 4 篇第六節)

五、引擎層設定的實際值

這些是容器內 modsecurity.conf 生效中的指令,都是映像檔預設,本安裝包只透過環境變數改了 audit 相關的幾項。

指令 值 意義
SecRuleEngine On 擋。改成 DetectionOnly 就只記錄
SecRequestBodyAccess on 讀請求本文(POST 表單、JSON)。關掉的話 POST 內的攻擊完全看不到
SecRequestBodyLimit / LimitAction 13107200 / Reject 請求本文超過 12.5 MB 直接拒絕(413)。nginx 這邊 client_max_body_size 0 是不限,所以實際上限是這一條
SecResponseBodyAccess on 讀回應本文,才能抓 SQL 錯誤訊息、原始碼外洩
SecResponseBodyMimeType text/plain text/html text/xml 只檢查這三種回應。JSON、圖片、下載檔不檢查(application/json 不在清單內,API 回應的外洩檢查是空的)
SecResponseBodyLimit / LimitAction 1048576 / ProcessPartial 回應本文只檢查前 1 MB,超過的部分照樣送出不檢查
SecAuditEngine RelevantOnly 只記相關交易
SecAuditLogRelevantStatus ^(?:5 4(?!04))
SecAuditLogParts ABCFHJZ 記哪幾段,第 3 篇第二節
SecAuditLogFormat JSON 一行一交易,Vector 用 parse_json 讀

六、異常分數:8 與 23 是怎麼算出來的

CRS 給每條偵測規則一個嚴重度,命中時依嚴重度加分到交易變數 tx.anomaly_score(請求)或 tx.outbound_anomaly_score(回應)。分數本身不擋人,擋人的是兩條評分規則:

嚴重度 加分 典型規則
CRITICAL(ModSecurity 等級 2) 5 942100 SQLi(libinjection)、941100 XSS(libinjection)、932 系列命令注入、930 系列路徑穿越
ERROR(等級 3) 4 部分協定攻擊、回應外洩 95x 系列多為此級
WARNING(等級 4) 3 920350 Host 標頭是 IP、920 系列多數協定強制規則、913 掃描器指紋
NOTICE(等級 5) 2 少數提示型規則
評分規則 看什麼 門檻(本安裝包)
949110 tx.anomaly_score,請求階段結束時 ANOMALY_INBOUND=5。達到就 deny,回 403,並在 audit.log 記一條 Inbound Anomaly Score Exceeded (Total Score: N)。這條訊息本身的等級是 0(EMERGENCY),第 3 篇會說這對平台端的嚴重度有什麼影響
959100 tx.outbound_anomaly_score,回應階段結束時 ANOMALY_OUTBOUND=4。達到就攔下回應,攻擊者看到的是 403 而不是後端的錯誤頁
980 系列 兩邊的總分 不擋,只在 phase 5 寫一條關聯摘要進 log

門檻 5 的意思是:一條 CRITICAL 就夠了,或是兩條 WARNING。下面是 2026-09-26 對防禦節點 8080 直打的實測(原始輸出在 /opt/tmp/verify/20260926-waf-doc-probe.log,位址已去識別化):

請求 命中規則與分數 總分 結果
GET /?id=1' OR 1=1-- 920350 Host 是 IP(+3,因為用 IP 直打)+ 942100 SQLi via libinjection(+5) 8 ≥ 5 403
GET /?q=alert(1) 920350(+3)+ 941100 XSS via libinjection(+5)+ 941110 script tag(+5)+ 941160 HTML injection(+5)+ 941390 JS method(+5) 23 ≥ 5 403
GET /auth/login(正常頁面,用 IP 直打) 920350(+3) 3 < 5 200,但 audit.log 有一行

第三列是理解「命中不等於阻擋」最好的例子:用 IP 而不是 hostname 打 WAF,每個請求都會命中 920350,3 分不夠擋,但 RelevantOnly 的條件是「有規則命中」,所以照樣寫 log、照樣被 Vector 讀走。內網驗證時看到一堆 920350 事件是這個原因,不是攻擊。從 Cloudflare 進來的請求 Host 是網域名,不會命中這條。

七、Paranoia Level

CRS 把規則分成四級偏執程度。等級越高,規則越多、抓得越寬、誤判越多。本安裝包預設 PL1,也就是 CRS 的出廠預設。

PL 多了什麼 代價
1 基礎規則:libinjection 的 SQLi/XSS 偵測、明顯的命令注入與路徑穿越、協定強制、掃描器指紋、回應外洩。CRS 宣稱這一級對正常應用「幾乎不誤判」 抓不到經過混淆或分散在多個參數的攻擊
2 更多字串型態的 SQLi/XSS 規則、對特殊字元密度的檢查、更嚴的 Content-Type 與編碼檢查 對「合法輸入含有 SQL 關鍵字」的應用(筆記、程式碼片段、報表查詢)開始誤判,需要寫排除
3 把 PL2 的關鍵字規則再放寬比對條件、限制參數數量與長度 多數正常應用都需要一批排除規則才跑得起來
4 幾乎所有非字母數字的參數值都會加分 適合高度受限的 API,一般網站不可用

兩個實務上要知道的事:第一,PL 在 .env 改 WAF_PARANOIA 後 --reconfigure 生效,但升級 PL 之前一定先用 DetectionOnly 跑一段時間看 audit.log 多出哪些規則(第 4 篇第三節)。第二,CRS 每條規則都帶 paranoia-level/N 標籤(audit.log 的 tags 欄位看得到),所以事後可以從 log 分辨「這條誤判是升級 PL 才出現的嗎」。

八、規則群一覽

這是容器內 rules/ 目錄的實際檔案,對應 CRS 4.25 的分組。原系列第 2 篇第二節有各群「抓什麼」的說明,這裡只補檔名與階段,方便對照 audit.log 的 file 欄位。

前綴 檔案 階段 備註
901 REQUEST-901-INITIALIZATION 請求 初始化 tx 變數、讀 crs-setup 的參數。覆寫檔的 900200 必須在它之前
905 REQUEST-905-COMMON-EXCEPTIONS 請求 CRS 內建的少數例外(Apache 內部 dummy 連線之類)
911 REQUEST-911-METHOD-ENFORCEMENT 請求 方法不在 tx.allowed_methods 內即 CRITICAL。本安裝包在覆寫檔補了 PUT/DELETE/PATCH/OPTIONS
913 REQUEST-913-SCANNER-DETECTION 請求 掃描器 User-Agent 與已知探測路徑,資料檔 scanners-user-agents.data
920 REQUEST-920-PROTOCOL-ENFORCEMENT 請求 協定強制:Host、Content-Length、編碼、副檔名、Content-Type。920350 在這裡
921 REQUEST-921-PROTOCOL-ATTACK 請求 request smuggling、response splitting、標頭注入
922 REQUEST-922-MULTIPART-ATTACK 請求 multipart 邊界異常
930 / 931 REQUEST-930-APPLICATION-ATTACK-LFI / 931-RFI 請求 本機/遠端檔案引入,資料檔 lfi-os-files.data、restricted-files.data
932 REQUEST-932-APPLICATION-ATTACK-RCE 請求 命令注入,資料檔 unix-shell.data、windows-powershell-commands.data
933 / 934 / 944 REQUEST-933-PHP / 934-GENERIC / 944-JAVA 請求 PHP 函式名、原型污染與 SSRF、Java 反序列化與 Log4Shell 類字串
941 REQUEST-941-APPLICATION-ATTACK-XSS 請求 XSS,含 libinjection 與多條字串規則。實測一個 命中四條
942 REQUEST-942-APPLICATION-ATTACK-SQLI 請求 SQLi,942100 是 libinjection
943 REQUEST-943-SESSION-FIXATION 請求 從參數塞 session id
949 REQUEST-949-BLOCKING-EVALUATION 請求 請求評分與攔截(949110)
950~956 RESPONSE-950-DATA-LEAKAGES、951-SQL、952-JAVA、953-PHP、954-IIS、955-WEB-SHELLS、956-RUBY 回應 回應外洩與 web shell 特徵,資料檔 sql-errors.data、web-shells-php.data 等
959 RESPONSE-959-BLOCKING-EVALUATION 回應 回應評分與攔截
980 RESPONSE-980-CORRELATION 記錄 寫關聯摘要,不擋
900 / 999 REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example、RESPONSE-999-…-AFTER-CRS.conf.example — CRS 給使用者放排除規則的兩個範本,副檔名是 .example 所以沒有載入。本安裝包的排除寫在覆寫檔,不用這兩個

上一篇
WAF 元件篇:nginx + ModSecurity v3 + OWASP CRS(一)
下一篇
WAF第 2 篇 一個請求在 WAF 容器內的旅程
系列文
一鍵完成六套開源防禦系統整合 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言