WAF 完整規則手冊
從零開始寫 WAF 規則,涵蓋全部 Field / Operator / Action,以及 JSON 模式與批量匯入匯出。
從零開始寫 WAF 規則,涵蓋全部 Field / Operator / Action,以及 JSON 模式與批量匯入匯出。
TL;DR 三條黃金原則:
- 白名單放最前面(優先級小數字),保證信任流量永遠通過
- 新規則先
log後block,看一週命中再決定是否升級- 預設規則保留,涵蓋了 80% 攻擊面
〇、JSON 模式 & 批量 JSON
新版面板對應每條規則的「新增 / 編輯」對話框增加 JSON 模式 Tab; 頁面頂部「自訂 WAF 規則」區段右上角新增 批量 JSON 按鈕。
兩個入口共享一套規則 JSON Schema,用法一致:你看到的就是後端真正存的東西。
JSON 模式(單規則)
進入 儀表板 > 域名 > 編輯 > WAF,點「自訂 WAF 規則」區段右上角 新增 / 列表上任一條規則的 編輯 鉛筆。對話框頂端有兩個 Tab:
- 表單:可視化點按填欄位(舊行為)。
- JSON 模式:直接編輯整條規則 JSON。
切換方向:
- 表單 → JSON:當前表單序列化成 JSON 顯示。
- JSON → 表單:嘗試解析 JSON,成功後回填表單;失敗則停留在 JSON tab 顯示錯誤, 原本表單內容不被覆蓋。
存檔時若停在 JSON tab,送出的是 JSON 內容;停在表單 tab,送出表單欄位。
批量 JSON 匯入 / 匯出
按頁面上的 批量 JSON 開啟對話框:
- 匯出:把當前所有「自訂」規則(不含預設規則)序列化成 JSON 陣列,可一鍵複製。 典型用途:把 A 域名的規則完整搬到 B 域名 / 跨環境備份。
- 匯入:貼入 JSON 陣列,逐條 POST 新增,不會覆蓋既有規則。
- 與既有同
(name, conditions, action)完全一致 → 後端回 409 拒絕,計入失敗數。 - 匯入後超過 10 條自訂上限 → 前端先擋,完全不送出。
- 部分失敗會在 toast 顯示失敗清單,成功的那幾條已寫入,不會自動 rollback。
- 與既有同
匯出/匯入用的是同一份 schema(下面第三節有完整參考);你可以匯出當作 template, 改改再貼回去當新規則。
一、規則結構(完整 Schema)
每條 WAF 規則的完整 JSON:
{
"name": "規則顯示名(必填,≤100 字)",
"description": "可選,≤500 字",
"action": "block",
"blockStatus": 403,
"blockBody": "Blocked by FurCDN WAF",
"rateLimitCount": 60,
"rateLimitWindow": 60,
"rateLimitBlock": 300,
"actionDurationSec": 0,
"priority": 10,
"enabled": true,
"conditions": [
{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/api/login" },
{ "field": "method", "operator": "equals", "value": "POST", "connector": "AND" }
]
}
]
}欄位逐項
| 欄位 | 型別 | 必填 | 預設 | 說明 |
|---|---|---|---|---|
name | string | ✓ | — | 規則名 |
description | string | null | 描述 | |
action | enum | ✓ | "block" | 見 §2 Action |
blockStatus | int 100–599 | 403 | action=block 回傳的狀態碼 | |
blockBody | string | "Blocked" | action=block 回傳的 body(≤4096 字) | |
rateLimitCount | int ≥0 | — | action=rateLimit 視窗內最大請求數 | |
rateLimitWindow | int ≥0 | — | action=rateLimit 視窗長度(秒) | |
rateLimitBlock | int ≥0 | — | action=rateLimit 觸發後封鎖時長(秒) | |
actionDurationSec | int 0–86400 | 0 | block/challenge/uam 的「黏性持續秒數」,見 §2.5 | |
priority | int ≥0 | 10 | 數字小的先評估 | |
enabled | bool | true | false 視為不存在 | |
conditions | group[] | ✓ | [] | 條件群組陣列,見 §4 |
後端限制
- 每網站自訂規則上限 10 條(預設規則不計入)
- 單條規則總條件數上限 5 個(跨 group 加總)
conditions序列化後 ≤32 KB- 同
(name, conditions, action)完全相同會被去重(回 409)
評估流程
- 規則按
priority數字小的先評估(同優先級不保證順序) - 條件命中 → 執行 action
allow→ 立即放行,跳過所有後續規則(白名單最強)block/challenge/uam→ 立即阻斷log/rateLimit不命中 → 繼續評估下一條規則- 全部規則都沒命中 → 預設放行
二、六種 Action
2.1 allow(白名單)
命中即放行,跳過後續所有規則。
{
"name": "公司白名單",
"action": "allow",
"priority": 1,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "ip", "operator": "cidr", "value": ["203.0.113.0/24", "198.51.100.0/24"] }
]
}]
}⚠️
allow是「徹底信任」,放行後連 rate limit、challenge、預設 block 都跳過。要謹慎用。
2.2 log(記錄)
命中只寫日誌不影響流量。新規則上線前先 log 看誤判率。
{
"name": "可疑 UA(觀察)",
"action": "log",
"priority": 100,
"enabled": true,
"conditions": [{
"logic": "OR",
"conditions": [
{ "field": "ua", "operator": "contains", "value": "curl" },
{ "field": "ua", "operator": "contains", "value": "wget", "connector": "OR" }
]
}]
}跑一週後到「訪問日誌」搜命中,確認沒誤判再改 block。
2.3 block(阻斷)
最常用。命中回 blockStatus(預設 403)+ blockBody。
{
"name": "禁止 .git 訪問",
"action": "block",
"blockStatus": 404,
"blockBody": "Not Found",
"priority": 5,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/.git" }
]
}]
}用 404 而非 403 可以「裝沒這個資源」,讓掃描器以為路徑不存在。
2.4 rateLimit(速率限制)
命中後計數,超過閾值就在窗口內封鎖。
| 參數 | 說明 |
|---|---|
rateLimitCount | 視窗內最大請求數 |
rateLimitWindow | 視窗長度(秒) |
rateLimitBlock | 觸發後封鎖時長(秒) |
{
"name": "登入速率限制",
"action": "rateLimit",
"rateLimitCount": 5,
"rateLimitWindow": 60,
"rateLimitBlock": 600,
"priority": 8,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/api/login" },
{ "field": "method", "operator": "equals", "value": "POST", "connector": "AND" }
]
}]
}計數 key 是
clientIP + rule.id,每條規則獨立計數。
2.5 Action 黏性
block / challenge / uam 三種有「黏性持續秒數」actionDurationSec(0–86400)。
0= 無黏性,每請求重新評估條件。>0= 條件第一次命中後鎖定該 action N 秒,期間跳過條件評估。
典型場景:challenge + actionDurationSec=300,可避免 QPS 抖動導致挑戰閃爍。
2.6 challenge(PoW 挑戰閘道)
命中 IP 進入 JS 挑戰頁,通過後簽 cookie。
- 真實使用者 < 1 秒解 PoW 自動跳轉,幾乎無感。
- 機器人/爬蟲沒有 JS runtime,無法通過。
{
"name": "API 區段加挑戰",
"action": "challenge",
"actionDurationSec": 300,
"priority": 50,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/api/" }
]
}]
}2.7 uam(5 秒盾)
跟 challenge 類似,但無條件擋 5 秒延遲後自動放行。比 challenge 寬鬆,適合輕度過濾。
三、十種 Field
ip
已套用 realIpHeader 後的真實客戶端 IP。
可選 operator:equals / cidr / in / notIn。
{ "field": "ip", "operator": "cidr", "value": "10.0.0.0/8" }
{ "field": "ip", "operator": "cidr", "value": ["1.2.3.0/24", "5.6.7.8/32"] }
{ "field": "ip", "operator": "in", "value": ["1.2.3.4", "5.6.7.8"] }uri
請求路徑,不含 query string(query 用下面的 queryParam)。
可選 operator:equals / contains / startsWith / endsWith / regex。
{ "field": "uri", "operator": "startsWith", "value": "/api/v1/" }
{ "field": "uri", "operator": "endsWith", "value": ".php" }
{ "field": "uri", "operator": "regex", "value": "^/files/[0-9a-f]{32}\\.zip$" }header
任意 HTTP header。需要額外的 headerName 欄位(大小寫不敏感)。
可選 operator:equals / contains / startsWith / regex。
{ "field": "header", "headerName": "Authorization", "operator": "startsWith", "value": "Bearer " }
{ "field": "header", "headerName": "X-Forwarded-For", "operator": "regex", "value": ".*,.*,.*" }body
請求 body。
可選 operator:contains / regex。
- 預設只讀前 64 KB;超過上限的 body 一律視為不命中(避免巨檔上傳卡 WAF)。
- 只在 POST/PUT/PATCH 有意義,GET 通常空 body。
{ "field": "body", "operator": "regex", "value": "(?i)union\\s+select" }ua(User-Agent 快捷)
等同 header + headerName="User-Agent",只是寫起來短。
可選 operator:equals / contains / startsWith / regex / in。
{ "field": "ua", "operator": "in", "value": ["sqlmap", "nikto", "acunetix"] }method
HTTP method。
可選 operator:equals / in。
{ "field": "method", "operator": "in", "value": ["DELETE", "PATCH", "PUT"] }referer
Referer header(header + headerName="Referer" 的快捷)。
可選 operator:equals / contains / startsWith / regex。
{ "field": "referer", "operator": "startsWith", "value": "https://example.com", "negate": true }queryParam
單一 query 參數。需要 queryParamName 欄位。
可選 operator:equals / contains / regex。
{ "field": "queryParam", "queryParamName": "redirect", "operator": "regex", "value": "^https?://(?!example\\.com)" }threatIntel(威脅情報)
是否命中威脅情報庫的特定來源。value 為來源 ID 字串:
| 來源 ID | 內容 |
|---|---|
spamhaus_drop | Spamhaus 全球已知惡意網段 |
spamhaus_edrop | Extended DROP(更廣) |
firehol_level1 | FireHOL 一級惡意 IP |
tor_exit | Tor 出口節點 |
self_feedback | 本平台 24h 高頻失敗 IP(自反饋) |
可選 operator:equals / notEquals / in / notIn。
{ "field": "threatIntel", "operator": "equals", "value": "tor_exit" }
{ "field": "threatIntel", "operator": "in", "value": ["spamhaus_drop", "firehol_level1"] }
{ "field": "threatIntel", "operator": "notEquals", "value": "" }最後一個寫法等同「命中任一來源」(空字串 = 未命中)。
requestRate(60 秒滾動請求速率)
過去 60 秒該 domain 的請求總數(節點端 rolling counter)。
可選 operator:gt / lt / gte / lte。value 為數字(QPS×60)。
{ "field": "requestRate", "operator": "gt", "value": 6000 }適合做「全站 QPS > N 時整段加挑戰」這種動態自動防護。
四、條件群組
群組(Group)
conditions 是群組陣列,群組之間是 OR(任一群組命中即規則命中)。
每個群組:
{
"logic": "AND", // 舊資料相容用 fallback,新規則建議用 connector
"conditions": [
{ "field": "...", "operator": "...", "value": "..." },
{ "field": "...", "operator": "...", "value": "...", "connector": "AND" },
{ "field": "...", "operator": "...", "value": "...", "connector": "OR" }
]
}群組內 connector
第二個及之後的 condition 都帶 connector: "AND" | "OR",從左到右逐條合併。
沒有運算子優先級,要強制邏輯就拆成多個 group。
- 第 1 條 condition 的
connector被忽略(沒有「前一條」可合併) - 沒寫
connector時 fallback 到group.logic(舊資料相容)
negate: true(取反)
所有 operator 都可加 negate: true 整條取反:
{ "field": "ip", "operator": "cidr", "value": "10.0.0.0/8", "negate": true }
// IP 不在 10.0.0.0/8多 group 範例
「(在 /admin/* 且不在白名單) OR (body 含 SQL 注入特徵)」:
{
"conditions": [
{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/admin/" },
{ "field": "ip", "operator": "cidr", "value": "10.0.0.0/8", "negate": true, "connector": "AND" }
]
},
{
"logic": "AND",
"conditions": [
{ "field": "body", "operator": "regex", "value": "(?i)union\\s+select" }
]
}
]
}五、預設規則
新建域名時自動產生若干條 isDefault: true 規則(掃描器攔截 / 空 UA / SQL 注入 / XSS / 路徑穿越等)。
- 預設規則不可刪除,只能 toggle 開關。
- 開關欄位是
defaultEnabled(不是enabled),前端會自動處理。 - 預設規則不計入 10 條自訂上限,也不會被批量匯出。
六、十條常用配方(可直接貼進「批量 JSON 匯入」)
把整段貼到 批量 JSON > 匯入 即可一次新增(注意 10 條上限,先匯出備份)。
[
{
"name": "辦公室白名單",
"action": "allow",
"priority": 1,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "ip", "operator": "cidr", "value": ["203.0.113.0/24", "198.51.100.0/24"] }
]
}]
},
{
"name": "命中威脅情報直接封",
"action": "block",
"blockStatus": 403,
"priority": 5,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "threatIntel", "operator": "in", "value": ["spamhaus_drop", "spamhaus_edrop", "firehol_level1"] }
]
}]
},
{
"name": "Tor 出口節點走 5 秒盾",
"action": "uam",
"actionDurationSec": 600,
"priority": 8,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "threatIntel", "operator": "equals", "value": "tor_exit" }
]
}]
},
{
"name": "禁止 .git / .env 訪問",
"action": "block",
"blockStatus": 404,
"blockBody": "Not Found",
"priority": 5,
"enabled": true,
"conditions": [{
"logic": "OR",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/.git" },
{ "field": "uri", "operator": "equals", "value": "/.env", "connector": "OR" },
{ "field": "uri", "operator": "endsWith", "value": "/.env", "connector": "OR" }
]
}]
},
{
"name": "登入端點嚴格限速",
"action": "rateLimit",
"rateLimitCount": 5,
"rateLimitWindow": 60,
"rateLimitBlock": 1800,
"priority": 15,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/api/auth/login" },
{ "field": "method", "operator": "equals", "value": "POST", "connector": "AND" }
]
}]
},
{
"name": "API 區段普通限速",
"action": "rateLimit",
"rateLimitCount": 60,
"rateLimitWindow": 60,
"rateLimitBlock": 300,
"priority": 20,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/api/" }
]
}]
},
{
"name": "圖片防盜鏈",
"action": "block",
"blockStatus": 403,
"priority": 30,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "regex", "value": "\\.(jpg|jpeg|png|gif|webp)$" },
{ "field": "referer", "operator": "startsWith", "value": "https://example.com", "negate": true, "connector": "AND" }
]
}]
},
{
"name": "PHP / WordPress 掃描器擋",
"action": "block",
"blockStatus": 404,
"priority": 5,
"enabled": true,
"conditions": [{
"logic": "OR",
"conditions": [
{ "field": "uri", "operator": "endsWith", "value": ".php" },
{ "field": "uri", "operator": "contains", "value": "/wp-admin/", "connector": "OR" },
{ "field": "uri", "operator": "contains", "value": "/wp-login.php", "connector": "OR" },
{ "field": "uri", "operator": "contains", "value": "/phpmyadmin/", "connector": "OR" }
]
}]
},
{
"name": "未帶 Bearer Token 擋管理 API",
"action": "block",
"blockStatus": 401,
"blockBody": "Unauthorized",
"priority": 25,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "uri", "operator": "startsWith", "value": "/api/admin/" },
{ "field": "header", "headerName": "Authorization", "operator": "startsWith", "value": "Bearer ", "negate": true, "connector": "AND" }
]
}]
},
{
"name": "全站 QPS > 6000 時加挑戰",
"action": "challenge",
"actionDurationSec": 300,
"priority": 90,
"enabled": true,
"conditions": [{
"logic": "AND",
"conditions": [
{ "field": "requestRate", "operator": "gt", "value": 6000 }
]
}]
}
]七、JSON 模式常見錯誤
| 錯誤訊息 | 原因 |
|---|---|
JSON 格式錯誤: Unexpected token | 多了 / 少了逗號、用了 trailing comma、雙引號漏掉 |
頂層必須是規則物件 | JSON 模式單規則編輯時頂層必須是 {...},不是 [...](陣列請走批量匯入) |
單條規則最多 5 個條件 | 整條規則所有 group 加總的 condition 數超過 5,拆規則 |
動作無效 | action 不在 block / allow / log / rateLimit / challenge / uam 之中 |
條件過大 | JSON.stringify(conditions) > 32 KB,正則太長或值列表太大,精簡 |
已存在相同規則 | 後端去重防呆:同 (name, conditions, action) 拒絕。改 name 或微調 conditions |
每個網站最多 10 條自訂 WAF 規則 | 達到 10 條上限,刪一條再加 |
八、故障排查
規則寫了但不生效
- 域名 WAF 開了嗎?域名編輯 > 基本設定 > WAF 開關
- 規則的
enabled=true嗎?(預設規則的開關欄位是defaultEnabled) - 優先級對嗎?可能被前面的
allow跳過了 - 看訪問日誌:該請求的「WAF 命中規則」欄位,有就是命中,沒就是真的沒匹配上
- field 拼寫:用
header+headerName: "User-Agent",不是header:user-agent
誤封正常使用者
- 看訪問日誌篩選 status
403/429 - 找到哪條規則命中
- 改成
log觀察一週,或加allow白名單 - 規則可能太嚴(如 SQL 注入規則誤判含
select關鍵字的正常 query)
Rate limit 觸發後永遠進不來
rateLimitBlock是封鎖時長(秒),設太大就會封很久- 計數 key 是
clientIP + rule.id,所以用戶換 IP 即可繞 - 短時間想解封:暫停規則 → 觸發過的封鎖會自動 expire
Challenge 通過後又被擋
- 黏性
actionDurationSec太短,QPS 抖動下重複觸發。設 60–600 秒 - 使用者瀏覽器禁 cookie / 禁 JS,會永遠在挑戰循環
JSON 模式存檔後表單不一致
切到 JSON 模式編輯後存檔,內部欄位以 JSON 為準。 重新打開規則時,後端載入的 conditions 字串會被解析回表單。 若你寫了 form 不支援的欄位(例如未來新增的擴充),表單編輯時可能會被丟棄, 要保留就全程用 JSON 模式編輯。
看不到想要的場景?開工單講具體需求,我們幫你寫規則。