FurCDN 文檔

WAF 完整規則手冊

從零開始寫 WAF 規則,涵蓋全部 Field / Operator / Action,以及 JSON 模式與批量匯入匯出。

從零開始寫 WAF 規則,涵蓋全部 Field / Operator / Action,以及 JSON 模式與批量匯入匯出。

TL;DR 三條黃金原則:

  1. 白名單放最前面(優先級小數字),保證信任流量永遠通過
  2. 新規則先 logblock,看一週命中再決定是否升級
  3. 預設規則保留,涵蓋了 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" }
      ]
    }
  ]
}

欄位逐項

欄位型別必填預設說明
namestring規則名
descriptionstringnull描述
actionenum"block"§2 Action
blockStatusint 100–599403action=block 回傳的狀態碼
blockBodystring"Blocked"action=block 回傳的 body(≤4096 字)
rateLimitCountint ≥0action=rateLimit 視窗內最大請求數
rateLimitWindowint ≥0action=rateLimit 視窗長度(秒)
rateLimitBlockint ≥0action=rateLimit 觸發後封鎖時長(秒)
actionDurationSecint 0–864000block/challenge/uam 的「黏性持續秒數」,見 §2.5
priorityint ≥010數字小的先評估
enabledbooltruefalse 視為不存在
conditionsgroup[][]條件群組陣列,見 §4

後端限制

  • 每網站自訂規則上限 10 條(預設規則不計入)
  • 單條規則總條件數上限 5 個(跨 group 加總)
  • conditions 序列化後 ≤32 KB
  • (name, conditions, action) 完全相同會被去重(回 409)

評估流程

  1. 規則按 priority 數字小的先評估(同優先級不保證順序)
  2. 條件命中 → 執行 action
  3. allow → 立即放行,跳過所有後續規則(白名單最強)
  4. block / challenge / uam → 立即阻斷
  5. log / rateLimit 不命中 → 繼續評估下一條規則
  6. 全部規則都沒命中 → 預設放行

二、六種 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$" }

任意 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_dropSpamhaus 全球已知惡意網段
spamhaus_edropExtended DROP(更廣)
firehol_level1FireHOL 一級惡意 IP
tor_exitTor 出口節點
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 / ltevalue 為數字(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 條上限,刪一條再加

八、故障排查

規則寫了但不生效

  1. 域名 WAF 開了嗎?域名編輯 > 基本設定 > WAF 開關
  2. 規則的 enabled=true?(預設規則的開關欄位是 defaultEnabled)
  3. 優先級對嗎?可能被前面的 allow 跳過了
  4. 看訪問日誌:該請求的「WAF 命中規則」欄位,有就是命中,沒就是真的沒匹配上
  5. field 拼寫:用 header + headerName: "User-Agent",不是 header:user-agent

誤封正常使用者

  1. 看訪問日誌篩選 status 403 / 429
  2. 找到哪條規則命中
  3. 改成 log 觀察一週,或加 allow 白名單
  4. 規則可能太嚴(如 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 模式編輯


看不到想要的場景?開工單講具體需求,我們幫你寫規則。

© 2023-2026 SLOWSPEED NETWORK LLC. 版權所有

langya.io 驅動

On this page