為什麼垃圾訊息打敗了我有速率限制的聯絡表單
為什麼垃圾訊息能穿過有速率限制的聯絡表單? 因為速率限制是依位址計算請求數,其中沒有任何一環會問是誰送出的。10月30日與11月2日,兩台弱點掃描器爬過我的網站,像任何訪客一樣載入放著表單的首頁,取得一個有效的 CSRF 權杖,然後不斷送出表單,直到它們離開或被封鎖為止。第一天有101筆提交通過,第二天31筆,每一筆都是應用程式回傳的200;寄出成功時它回傳200,寄出失敗時也回傳200,所以這些數字是它們最多可能產生的電子郵件數。12 我的應用程式限流器設定為每個位址每分鐘五次,它拒絕了這兩台掃描器的830與31次送出,放行了101與31次寄送,比在同樣設定下精確運作的限流器多放了47與9次。3 edge 的速率限制對兩台掃描器都沒有觸發:第一波洪泛的最後一次表單送出之後大約12分鐘,我才建立這條規則;而第二台掃描器的送出經由兩個 Cloudflare 資料中心抵達,兩者各自保有獨立的計數器。這種分流是我對第二次沉默最有力的解釋:它與每分鐘的總數相符,但每分鐘的總數無法證明它。456 在我的工作室網站上,一個蜜罐欄位在八天內攔下52筆提交中的42筆,放行了寄件者讓蜜罐保持空白的10筆。78 本站的 edge 確實有一條能詢問「是否有人在場」的規則,也就是針對表單路徑的受管質詢(managed challenge),但它只在列出的 user-agent 字串出現時觸發,而兩台掃描器送出的都是瀏覽器的字串;第二個質詢以高威脅分數為條件,卻永遠不可能觸發,因為 Cloudflare 現在把這個分數固定設為零,所以沒有任何東西會對每一筆提交提出這個問題。910411 自11月3日早上起,Turnstile 會對兩份表單上每一筆可能抵達我收件匣的提交提出這個問題。12
重點摘要
- 兩台掃描器,一個收件匣。 10月30日,一個位址在53個一分鐘時段內對我的網站送出76,429個請求;11月2日,另一個位址在22個時段內送出10,907個。兩者都爬過整個網站,都在路徑與 User-Agent 標頭中送出 SQL 注入字串,也都送出聯絡表單:第一天1,065次,第二天500次。131412
- 有效的 CSRF 權杖只證明用戶端持有我網站簽發的權杖。 兩天的每一次寄送都帶著一個,因為我的權杖是無狀態的,任何抓取頁面的用戶端都能拿到。在兩波洪泛中,CSRF 檢查只拒絕了四次送出。1516
- 我自己的限流器會漏。 它從 Cloudflare KV 讀取計數器、比較,然後再讀一次,並在另一個請求中寫入計數加一;第二次讀取失敗時會寫入一,而 KV 呼叫失敗時則退回到單一行程記憶體中的計數。這些路徑中的任何一條都可能放行超過五次,而日誌無法說明是哪一條:在上限為五的情況下,某一分鐘寄出了18封。171
- edge 速率限制是依位址、依資料中心計數。 11月2日從18:56到19:05:00 UTC,也就是封鎖前後的幾分鐘內,掃描器的85次表單送出經由阿姆斯特丹與倫敦抵達,分別為44與41次,最忙的一分鐘在兩者之間分成18對14。這種分流是規則保持沉默最可能的原因,儘管每分鐘的總數無法證明。隔天早上我進行的一次探測證明,這條規則會觸發,而且 Cloudflare 會記錄它。518
- 封鎖都是位址封鎖,而且兩次都來得太晚,擋不住電子郵件。 10月30日,我在發現後3.9分鐘手動封鎖該位址,距它第一次送出表單大約50分鐘。11月2日,我的監控程式在掃描第一分鐘之後8分鐘封鎖了它。兩天都沒有任何寄送發生在封鎖那一分鐘之後:10月30日的最後一次寄送比封鎖早了超過12分鐘;11月2日則有26次落在封鎖之前的幾分鐘,5次落在封鎖當分鐘,而每分鐘的資料無法判斷這5次是在封鎖那一秒之前還是之後。101912
- 蜜罐攔下會填滿每個欄位的機器人。 在我的工作室網站上,它對52次送出中的42次回應得彷彿成功,卻什麼也沒有儲存;依抵達的小時判斷,每一筆來自 Tor 出口節點的提交都在其中。被儲存的10筆,也就是最多10封電子郵件,來自讓蜜罐保持空白的寄件者。720
- 改了什麼。 兩份表單現在都帶有受管模式的 Cloudflare Turnstile 小工具,在 Cloudflare 的 siteverify API 接受提交的權杖之前,伺服器不會儲存或寄出任何東西,並拒絕缺少、過長或無法驗證的權杖。Turnstile 在訪客的瀏覽器中執行質詢,並處理 IP 位址、TLS 指紋與 user agent 等訊號;Cloudflare 表示它不會碰觸表單內容。它擋不住手動輸入垃圾訊息的人,也不取代速率限制,後者仍然為流量設下界線。122122
1. 問題:「是你嗎?」
11月2日,我的收件匣塞滿了聯絡表單訊息,而我的第一個問題,是問負責管理我基礎設施的代理程式工作階段:是不是我自己的自動化在寄這些信?不是。寄信的是一台掃描器,透過本站的聯絡表單;我工作室網站 941apps.com 上的表單也正承受一股屬於它自己的緩慢滴流,不過那天沒有任何一則抵達收件匣。238 三條郵件串流,也就是我監控程式的警報和兩份表單,都從我自己郵件網域上的同一個位址寄出,所以在收件匣裡看起來一模一樣。23
主旨洩了底。本站的聯絡表單處理程式會把每筆提交寄給我,主旨是「Contact Form:」加上寄件者在姓名欄位裡輸入的任何內容,因此掃描器的酬載就變成了主旨。2423 工作室網站的表單則把每則訊息存進資料庫,再寄到同一個收件匣。25
我最先問的問題問錯了,而正確的問題花了一天的查詢才得到答案:這些表單上的控制機制中,哪些能夠分辨掃描器與人,而每一個又為什麼放行了其餘的?控制機制共有五種。在本站:一道帶有自訂規則的 edge 防火牆、一個 edge 速率限制、一個 CSRF 權杖,以及應用程式中的限流器。在工作室網站:同類的 edge 規則、一個 edge 速率限制、一個自己的應用程式限流器(它沒有拒絕任何一次觀察到的表單送出),以及一個蜜罐欄位。9257 本文其餘部分依序處理兩波洪泛和那股緩慢滴流,再逐一處理這五層,每一層都附上規則文字或程式碼的行為。
數字從哪裡來
以下每一個計數,都來自 Cloudflare 針對兩個區域(zone)的請求分析與防火牆事件,於11月3日針對已經結束的時間窗查詢,再加上工作室網站的訊息資料表與我監控程式自己的日誌。16 一支腳本把這些儲存下來的查詢結果整理成一本幾百列的帳本,每一列都附上產生它的規則,本文的數字只取自這本帳本及其附錄,別無他處。當我當天的筆記與帳本不一致時,以帳本為準:10月30日的筆記是我在洪泛結束後幾分鐘寫的,當時 Cloudflare 還在匯入最後幾分鐘的資料,所以筆記少算了。1626 Cloudflare 保留這些資料集「at least 31 days of data on every plan」(每種方案都至少保留31天的資料),所以我能在事後清點一波洪泛,但不能無限期地這樣做。27
有兩種數字需要先提醒。防火牆事件是抽樣資料,所以我用它判斷是哪條規則、在什麼時候動作,至於數量則用請求資料集。16 此外,本站一次回應200的表單送出,代表的是一次寄送,或是處理程式同樣以200回應的失敗(它有兩條這樣的失敗路徑),而不是一封已送達的電子郵件,我也沒有查看郵件服務的日誌。因此本文中的每一個電子郵件計數都是上限。2428
平常是什麼樣子
洪泛之前,表單很安靜。在查詢涵蓋的八天裡,除了兩台掃描器之外,有三個位址成功送出過表單,每個都恰好兩次,10月27日、28日、29日各一個位址。29 我不知道那些是不是人。同一個位址送出兩次,符合雙擊,也符合會重試的機器人。這份不確定性就是整個問題的縮影:日誌和控制機制都無從判斷。
2. 第一波洪泛:10月30日,手動封鎖
第一波洪泛來自一個位址,所屬網路在區域網際網路註冊管理機構的登記名稱為 BULLIONET FBW NETWORKS SAS,註冊於法國。30 它的第一個請求落在 UTC 17:59 這一分鐘,即太平洋夏令時間10:59,最後一個落在18:51這一分鐘,即 PDT 11:51:53個一分鐘時段,對本站共76,429個請求。兩項獨立查詢,一項是針對該位址的逐分鐘查詢,一項是掃過我帳號中每個區域的查詢,都得到相同的總數,而該位址在這段時間之前和之後都沒有送出任何東西。13
這次掃描的樣貌
表單只是其中一小部分。封鎖之前,該位址平均每分鐘1,389.7個請求,在18:36這一分鐘達到3,219個的高峰,而且走遍了整個網站:至少2,881條不同的路徑,這是下限,因為路徑查詢觸及了列數上限。1331 整次掃描的結果分布如下:13
| 結果 | 請求數 | 占比 |
|---|---|---|
| 已提供(2xx、3xx) | 49,513 | 64.8% |
| 被應用程式拒絕(4xx),其中10,019個來自網站的代理程式閘門 | 18,990 | 24.8% |
| 來源伺服器錯誤(520) | 2,339 | 3.1% |
| 在 edge 被封鎖(403) | 5,587 | 7.3% |
edge 的7.3%幾乎全來自封鎖:5,587次拒絕中有5,234次發生在封鎖那一分鐘之後。13 在封鎖前的五十分鐘裡,只有漏洞利用路徑規則有動作。這條規則從第一分鐘起就在運作,第一筆防火牆事件出現在 UTC 17:59:53,但它比對的是 .php 和 /wp- 之類的路徑子字串,而這次掃描大多請求的是真實的頁面。109
讓它成為一次掃描而不只是爬取的,是它放進請求裡的東西。幾乎所有流量,至少69,322個請求,都使用同一個普通的 Windows 版 Chrome 141 user agent。另有至少2,755個請求自稱是 Googlebot,而 edge 的已驗證機器人規則並未把它們當成 Google。32 至少3,154個請求在 User-Agent 標頭本身帶著 SQL 注入酬載,涵蓋至少396個不同字串;另有至少2,680個請求把酬載放在路徑中,橫跨至少810條不同的路徑。3231 這些酬載屬於有文件記載的家族。PG_SLEEP(15)、waitfor delay '0:0:15' 與 XOR(if(now()=sysdate(),sleep(15),0)) 是基於時間的探測:PortSwigger 的速查表列出了 Oracle、Microsoft SQL Server、PostgreSQL 與 MySQL 的延遲語法,而 sqlmap 自己的酬載定義中也包含同樣的函式。3334 PortSwigger 說明了它們為什麼有效:「As SQL queries are normally processed synchronously by the application, delaying the execution of a SQL query also delays the HTTP response.」(由於應用程式通常同步處理 SQL 查詢,延遲 SQL 查詢的執行也會延遲 HTTP 回應。)35 一次緩慢的回應證明不了什麼;每次酬載要求時都會出現約15秒的延遲,而在不帶酬載的對照請求上不會出現,這才是可能存在注入的證據。
本站的繪圖應用程式有一個分享端點,而掃描器找到了它。它對分享路徑送出了320個 POST,是查詢涵蓋的八天裡唯一在那裡送出 POST 的位址,而端點拒絕了全部320個,什麼也沒有儲存。在這些請求周圍,它還對內嵌酬載的路徑變體送出 POST,例如 /paint/share'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||' 與 /paint/(select 198766*667891 from DUAL),網站對這些回應404。31 該位址也碰觸了我帳號中的另一個區域,對 kiradex.app 送出387個 GET,全部回應200,此外沒有其他區域。13 我的解讀是它順著本站連到那個網站的連結過去;我沒有檢查那些連結。
OWASP 的自動化威脅目錄為這種活動取了名字:OAT-014,Vulnerability Scanning(弱點掃描),「Crawl and fuzz application to identify weaknesses and possible vulnerabilities.」(爬取應用程式並進行模糊測試,以找出弱點與可能的漏洞。)36 聯絡表單只是又一個可以拿來模糊測試的輸入。
表單,逐分鐘
該位址送出聯絡表單1,065次,每一次都抵達了應用程式;edge 一次也沒有攔下。1 第一次送出和第一次寄送都發生在 UTC 18:00 這一分鐘,即 PDT 11:00。以下是每一個有表單送出抵達應用程式的分鐘,依應用程式回傳的狀態列出:1
| UTC | PDT | 已寄出(200) | CSRF 拒絕(403) | 缺少欄位(422) | 限流器拒絕(429) |
|---|---|---|---|---|---|
| 18:00 | 11:00 | 8 | |||
| 18:01 | 11:01 | 12 | 133 | ||
| 18:02 | 11:02 | 9 | 148 | ||
| 18:03 | 11:03 | 4 | 4 | 200 | |
| 18:04 | 11:04 | 9 | 89 | ||
| 18:05 | 11:05 | 16 | 164 | ||
| 18:06 | 11:06 | 18 | 85 | ||
| 18:07 | 11:07 | 7 | 11 | ||
| 18:08 | 11:08 | 2 | |||
| 18:09 | 11:09 | 4 | |||
| 18:10 | 11:10 | 9 | |||
| 18:11 | 11:11 | 1 | |||
| 18:12 | 11:12 | 1 | |||
| 18:35 | 11:35 | 1 | |||
| 18:36 | 11:36 | 1 | |||
| 18:37 | 11:37 | 129 |
總計:101次寄出,830次被應用程式的限流器拒絕,130次因缺少必填欄位而被拒絕,4次被 CSRF 檢查拒絕,四次全在18:03這一分鐘。1 那些429來自應用程式。edge 的速率限制規則當時還不存在,該位址沒有任何速率限制防火牆事件,而在 Cloudflare 有記錄這些送出之來源伺服器狀態的地方,429都來自來源伺服器。1037 那些422來自 FastAPI 的表單驗證,它在限流器之前執行,所以從未消耗限流器的名額;我用一個十行的 FastAPI 應用程式確認了這個順序:缺少必填欄位的送出回傳422,包裝函式完全沒有被呼叫。38 18:37那一陣129次,看起來像是掃描器藉由刪除欄位來對表單欄位做模糊測試。
把「已寄出」這一欄對照限流器來看。限流器允許每個位址在每個時鐘分鐘內送出五次,而在其中八分鐘裡寄出的超過五封,18:06更多達18封。13 第4節會說明原因。
發現與封鎖
我在 PDT 11:46 發現異狀,寫下有東西一再打我網站的聯絡表單。26 那時第一次表單送出已經過了大約46分鐘,而表單洪泛本身在 PDT 11:37 就已結束。1 我手動把該位址加入帳號層級的封鎖清單,這份清單本來就由該區域的第一條自訂規則比對,而這條規則的第一筆事件出現在 UTC 18:49:53,即 PDT 11:49:53。10 封鎖發生在我發現後3.9分鐘,距第一次表單送出大約50分鐘,距掃描的第一分鐘50.9分鐘。113 全部101次寄送都在封鎖之前就已發出;最後一次抵達應用程式的表單送出比封鎖早了大約12分鐘。1 封鎖那一分鐘之後,該位址又送出了5,234個請求,edge 全數拒絕。13
就在同一秒,UTC 18:49:53,我建立了該區域唯一的速率限制規則:對聯絡與繪圖分享路徑的表單 POST,每個位址每10秒3次,封鎖10秒。9 在 UTC 18:52:38,我從自己的電腦測試它。根據那次測試的筆記,相隔一秒、帶瀏覽器 user agent 且沒有有效 CSRF 權杖的六次送出「returned 403 403 403 429 429 429 (the app’s CSRF rejection, then the edge’s rate limit)」(回傳403 403 403 429 429 429,先是應用程式的 CSRF 拒絕,接著是 edge 的速率限制),而單純用 curl 的送出則根本沒碰到這條規則,因為一條既有的自訂規則改為對它們送出受管質詢。26 我的筆記說那些送出沒有帶權杖,但缺少該欄位的送出會在處理程式執行前就得到422,這是我11月3日的探測所顯示的,所以那些403代表的是有帶權杖但權杖無效。18 防火牆事件與此一致:來自我測試位址的三筆速率限制事件,第一筆就在那一秒,另有24筆針對我 curl 送出的受管質詢。39 我的測試流量都沒有算進上面任何一個計數。
那天我的監控程式沒有在運作。我在10月31日重新啟用它,而它的流量偵測器是在這波洪泛之後才加上的。19 10月30日的封鎖,是一個人看著收件匣做出來的。
10月30日:來自同一個位址的76,429個請求,逐分鐘;在我手動封鎖該位址之前,edge 幾乎一個也沒有拒絕。1310
3. 第二波洪泛:11月2日,由我的監控程式封鎖
第二個位址屬於 NovoServe B.V.,一家位於荷蘭的主機代管業者。40 它在 UTC 07:49,即11月1日太平洋標準時間23:49,送出兩個 GET,之後11.1小時毫無動靜。正式的掃描從 UTC 18:56 這一分鐘持續到19:17這一分鐘,即 PST 10:56 到11:17:22個一分鐘時段,共10,907個請求,加上稍早那兩個 GET 則是10,909個。四項獨立查詢得到相同的總數。14
它看起來像是同一套工具。它在9,725個請求中使用與10月30日那個位址相同的 Windows 版 Chrome 141 user agent 字串,在942個請求中使用相同的 Googlebot 字串,同樣的注入家族,而且碰觸的也是同樣兩個區域、沒有其他,對 kiradex.app 送出368個 GET。414214 兩個國家、兩個網路上的兩個位址,證明不了是誰在操作它們,所以我只說到「看起來像」為止。
時間軸,兩個時區並列
| UTC | PST | 發生了什麼 |
|---|---|---|
| 18:56 | 10:56 | 掃描開始:這一分鐘有29個請求。1419 |
| 18:57:00 | 10:57:00 | 我的監控程式開始一次執行。它的時間窗在18:57:22結束,因此只看到大約一分鐘的掃描,什麼也沒發現。19 |
| 18:57 | 10:57 | 封鎖前最忙的一分鐘,637個請求。edge 的漏洞利用路徑規則從18:57:11開始封鎖。144 |
| 18:58 | 10:58 | 前三次表單送出。應用程式三次全部寄出。2 |
| 19:00 | 11:00 | 又有四次表單送出,四次全部寄出。2 |
| 19:02 | 11:02 | 32次表單送出抵達應用程式:9次寄出,23次被應用程式的限流器拒絕。2 |
| 19:02:46 | 11:02:46 | 監控程式的下一次執行開始。19 |
| 19:03 | 11:03 | 10次寄出,8次被拒絕。2 |
| 19:03:53 | 11:03:53 | 我的監控程式提出一筆嚴重等級的請求洪泛發現:該位址有1,914個請求。19 |
| 19:04:01 | 11:04:01 | 我的監控程式把該位址加入帳號封鎖清單。19 |
| 19:04:03 | 11:04:03 | 封鎖清單規則的第一筆事件。4 |
| 19:04 | 11:04 | 最後五次寄送,以及在 edge 被拒絕的22次表單送出,全都落在封鎖當分鐘;分鐘時段無法說明哪個先發生。2 |
| 19:07 | 11:07 | 整次掃描最忙的一分鐘,1,107個請求,全部被封鎖。14 |
| 19:17 | 11:17 | 最後一分鐘,300個請求,全部被封鎖。14 |
我的監控程式的八分鐘
我的監控程式在掃描第一分鐘之後7.9分鐘提出第一筆發現,封鎖在8.0分鐘後生效,也就是第一筆防火牆事件之後6.8分鐘。194 到那時,已有26次寄送在封鎖之前的幾分鐘內發出,最後5次落在封鎖當分鐘,而每分鐘的資料無法判斷它們在封鎖那一秒之前還是之後;之後的分鐘裡沒有任何寄送。2 對封鎖來說,八分鐘算快;對收件匣來說,八分鐘太慢。
PST 10:57:00 開始的那次執行,視野中只有大約一分鐘的掃描,而在它的時間窗內,該位址發出的請求介於29到666個之間;上限把18:57整分鐘都算進去,而時間窗只看到其中22秒。偵測器沒有記錄任何發現。19 下一次執行,在 PST 11:02:46,發現了洪泛並封鎖了它,而這次執行正好說明了第一次為什麼會漏掉。它的時間窗在它向 Cloudflare 查詢的那一刻結束,此時最新的事件還沒到達:它的日誌記錄該位址有1,914個請求,而我在11月3日針對同一個五分鐘時間窗、以完整資料重播,計得2,143個;這次重播連同指令與完整輸出都已保存,它的計數是帳本附錄中的列。1943 重播還提出了一筆即時執行從未提出的表單洪泛發現,依據是39次送出,其中23次被拒絕,屬於高(high)嚴重等級,原本會是每日摘要中的一行,而不是一封電子郵件或一次封鎖。43 我重播的是第二次執行的時間窗,不是第一次的,所以對於 PST 10:57 那次執行,同樣的延遲是可能的原因,而非量測到的原因。同一天,我把監控程式的時間窗改為在每次執行前90秒結束,並從上一次結束的地方開始。43 該位址總共觸發了六筆發現,而我的監控程式把每一筆都併入每日摘要,而不是寄電子郵件給我,因為該位址已經在它的封鎖帳本上。19 我的監控程式應該如何發出警報,以及從它那一側看這台掃描器,是另一篇文章的主題;就本文而言,重要的是封鎖是一種反應,而這個反應發生在31次寄送中至少26次之後。
表單,依狀態
該位址送出表單500次。2
| UTC | PST | 已寄出(200) | 限流器拒絕(429) | edge 封鎖(403) |
|---|---|---|---|---|
| 18:58 | 10:58 | 3 | ||
| 19:00 | 11:00 | 4 | ||
| 19:02 | 11:02 | 9 | 23 | |
| 19:03 | 11:03 | 10 | 8 | |
| 19:04 | 11:04 | 5 | 22 | |
| 19:05 | 11:05 | 39 | ||
| 19:06 | 11:06 | 19 | ||
| 19:07 | 11:07 | 10 | ||
| 19:08 | 11:08 | 13 | ||
| 19:09 | 11:09 | 31 | ||
| 19:10 | 11:10 | 41 | ||
| 19:11 | 11:11 | 47 | ||
| 19:12 | 11:12 | 97 | ||
| 19:13 | 11:13 | 29 | ||
| 19:14 | 11:14 | 23 | ||
| 19:15 | 11:15 | 21 | ||
| 19:16 | 11:16 | 28 | ||
| 19:17 | 11:17 | 18 |
總計:31次寄出,31次被應用程式的限流器拒絕,438次在 edge 被拒絕,全部發生在封鎖當分鐘或之後。所以有62次送出抵達了應用程式。2 每一次寄送都證明掃描器持有有效的 CSRF 權杖,因為處理程式在寄送之前會檢查權杖。那些429則不證明任何事,因為限流器在處理程式讀取權杖之前就拒絕了送出。2416 在 Cloudflare 有記錄這些429之來源伺服器狀態的地方,狀態都來自來源伺服器,也就是應用程式的限流器,而且該位址沒有任何速率限制防火牆事件。374
掃描的其餘部分
依結果區分,11月2日的掃描分布如下:14
| 結果 | 請求數 | 占比 |
|---|---|---|
| 已提供(2xx、3xx) | 1,388 | 12.7% |
| 被應用程式拒絕(4xx),其中420個來自代理程式閘門 | 1,473 | 13.5% |
| 來源伺服器錯誤(520) | 116 | 1.1% |
| 在 edge 被封鎖(403) | 7,930 | 72.7% |
edge 的72.7%大部分來自封鎖:2,986個請求發生在封鎖那一分鐘之前,7,445個在之後,網站一個也沒有提供給後者。14 這一天的路徑查詢是完整的:2,173條不同的路徑,其中542條上的778個請求帶有 SQL 注入酬載。44 user-agent 查詢也是完整的,共73個不同字串。除了那個普通的 Chrome 字串和 Googlebot 自稱之外,掃描器還在149個請求中送出54個不同的 SQL 酬載,在78個請求中送出三個用來截斷引號的字串,以及12個標記字串(marker token),每個各送出一次。41
其中幾個字串值得一看,因為它們顯示了掃描器在問什麼。(select 198766*667891 from DUAL) 是一個在不含資料表的 select 中的算術運算式;FROM dual 是 Oracle 寫這種查詢的語法,如 PortSwigger 的速查表所示。33 if(now()=sysdate(),sleep(15),0) 的形狀與同一份速查表上條件式時間延遲的 IF(condition, SLEEP(n), x) 相同。33 @@qkLpP 是一個 @@ 前綴,也就是速查表在 Microsoft 與 MySQL 的 SELECT @@version 中使用的那個前綴,後面接五個字元,而掃描器送出了十幾個類似的字串。3341 OWASP 的測試指南為這些字串所服務的兩種技術命名:錯誤型注入,它「forces the database to generate an error」(迫使資料庫產生錯誤),以及時間延遲,它們「use database commands (e.g. sleep) to delay answers in conditional queries.」(使用資料庫指令,例如 sleep,在條件式查詢中延遲回應。)45 我抓取的頁面中沒有一頁記載這些確切的字串,所以我只描述它們,不把它們歸給特定的工具。
11月2日:來自同一個位址的10,907個請求,逐分鐘,並標出我監控程式的兩次執行,以及它在 PST 11:04:01 施加的封鎖。1419
4. 五層防護,以及每一層能分辨什麼
五種控制機制擋在這些機器人與我的收件匣之間。對每一種,我都問同一個問題:它能分辨機器人和人嗎?edge 規則中有一個受管質詢能向訪客提出這個問題,但只針對 user-agent 字串在其清單上的請求,而掃描器的字串不在清單上;第二個以威脅分數為條件的質詢永遠不可能觸發,所以沒有任何東西會對每一筆提交提出這個問題。其中兩種控制機制負責計數,一種比對名稱與清單,一種證明用戶端持有網站簽發的權杖,還有一種只攔下會填寫隱藏欄位的機器人。以下逐一說明每一種檢查什麼、攔下了什麼,以及它放行其餘部分的確切原因。
edge 自訂規則
它們檢查什麼。 在本站,第一條自訂規則封鎖帳號層級清單上的任何位址,外加依 ASN 指定的七個主機代管網路。另一條封鎖 .php、/wp- 與 /.env 之類的漏洞利用路徑子字串。還有兩條對聯絡表單和管理路徑送出受管質詢:一條針對空的 user agent,或包含 curl、wget、python-requests、httpx、scrapy、HeadlessChrome、PhantomJS 或 Selenium 的 user agent;另一條針對高 Cloudflare 威脅分數。工作室網站有同類的規則,兩個質詢都套用到整個網站,另有一條封鎖 API 路徑、但豁免聯絡表單的規則。9 那些威脅分數質詢,每個網站各一條,都是死規則:Cloudflare 的欄位參考說,這個分數過去介於0到100之間,而「Now, the threat score is always 0 (zero).」(現在,威脅分數一律為0。)要求高分數的規則什麼也比對不到,我在讀到這段文字的11月3日當天停用了這兩條;user-agent 質詢則保留。1146 Cloudflare 的 Free 方案允許五條自訂規則,這些規則「work as customized web application firewall (WAF) rules that you can use to perform actions like Block or Managed Challenge on incoming requests.」(作為自訂的網頁應用程式防火牆 (WAF) 規則運作,您可以用它們對傳入請求執行 Block 或 Managed Challenge 等動作。)47
它們攔下了什麼。 兩天都從第一分鐘起攔下了漏洞利用路徑。104 10月30日,一次表單送出也沒攔下。110 11月2日,攔下了我監控程式封鎖之後的438次表單送出。2 在工作室網站上,一次表單送出也沒攔下:在那八天裡,防火牆沒有對表單的 POST 記錄任何事件。7
其餘為什麼能通過。 沒有任何一條規則會去看請求是什麼。清單規則只有在某個東西把位址放上清單之後才會比對到它,10月30日是一個人,11月2日是我的監控程式。91019 user-agent 質詢比對的是一個字串,而兩台掃描器送出的都是瀏覽器字串,第一天至少69,322次,第二天9,725次。3241 user-agent 質詢對會自報身分的工具確實有效:我自己10月30日的 curl 測試招來了24次受管質詢。39 威脅分數質詢對任何人都無法觸發,而防火牆對兩個位址都沒有在表單上記錄任何質詢。11104 Tor 出口節點,以及七個指名 ASN 之外的主機代管網路,都不在任何清單上。
edge 規則能把已知的位址、已知的網路或誠實的工具與其餘區分開來,而它的質詢只會對其運算式選中的請求詢問是否有人在場;它無法分辨掃描器送出的瀏覽器字串與真正的瀏覽器。
edge 速率限制
它檢查什麼。 規則文字很短:9
(http.request.method eq "POST" and http.request.uri.path in {"/api/contact" "/paint/share"})
它依 Cloudflare 資料中心與用戶端位址計算符合條件的請求,允許每10秒3次,並封鎖10秒。若平均分布,這個上限名義上相當於每個位址、每個資料中心每分鐘18次;規則本身則是以10秒為週期計數。916 Free 方案固定了週期、逾時與計數鍵,並只給區域一條這樣的規則:10秒的計數週期、10秒的緩解逾時,以及依 IP 計數。48 資料中心也不是可選的。Cloudflare 的文件寫道:「Every rate limiting rule includes the Cloudflare data center ID (cf.colo.id) as a mandatory characteristic」(每條速率限制規則都包含 Cloudflare 資料中心 ID (cf.colo.id) 作為必要特性),以及「Counters are not shared across data centers, with the exception of data centers associated with the same geographical location.」(計數器不會在資料中心之間共用,與同一地理位置相關聯的資料中心除外。)6
它攔下了什麼。 兩天都沒有攔下任何掃描器請求。它在事件時間窗內唯一的事件,是我自己測試產生的那三筆。10439 工作室網站有一條自己的全站速率限制規則,計數方式相同,涵蓋除 Cloudflare 列為已驗證機器人之用戶端以外的所有請求,而它沒有拒絕這股滴流中的任何一次表單送出:在那八天裡,它的防火牆沒有對表單的送出記錄任何事件。那條規則計算的是一個位址送往網站的每一個此類請求,而我的資料只計算表單送出,所以無法衡量計數器還剩下多少空間。97
其餘為什麼能通過。 10月30日,這條規則直到 UTC 18:49:53 才存在,與手動封鎖的第一筆事件同一秒,大約在最後一次表單送出抵達應用程式之後12分鐘。91 那台掃描器的1,065次送出全部經由同一個資料中心,巴黎,所以如果規則當時已經存在,它會用一個計數器計算它們。5 11月2日,規則已經存在,而掃描器的表單送出從 UTC 18:56 到19:05:00 經由兩個資料中心抵達,阿姆斯特丹44次,倫敦41次。在最忙的一分鐘19:02,倫敦承載18次,阿姆斯特丹14次。5 一分鐘18次名義上相當於規則的上限,但我的資料是每分鐘的總數,不是抵達時間,所以無法顯示是否有任何一個資料中心的某個10秒週期內超過三次。Cloudflare 對精確度直言不諱:「Rate limiting rules are not designed to allow a precise number of requests to reach your origin server. There may be a delay of up to a few seconds between detecting a request and updating rate counters.」(速率限制規則並非設計為讓精確數量的請求抵達您的來源伺服器。從偵測到請求到更新速率計數器之間,可能會有最多數秒的延遲。)48 從第一次抵達應用程式的送出到最後一次,在這七分鐘的跨度內,掃描器平均每分鐘8.86次。2 我當天的筆記把它稱為「about nine a minute」(每分鐘大約九次),然後就停在那裡;跨資料中心的分流正是筆記遺漏的部分。23
我想證明這條規則到底有沒有作用,所以在11月3日早上從自己的電腦探測它。在 PST 06:36:49,我以0.4秒的間隔送出十個不帶 CSRF 權杖的 POST:四個回傳422,因為缺少必填欄位,被 FastAPI 拒絕,接著六個回傳429。在 PST 06:38:03,我送出八個所有欄位都齊全的 POST:三個回傳403,也就是來源伺服器的 CSRF 拒絕,接著五個回傳429,內文是 Cloudflare 的「error code: 1015」。在那幾分鐘裡,防火牆對表單記錄了11筆事件,全都是速率限制規則的封鎖。18 規則會觸發,而 Cloudflare 會記錄它。我對11月2日的解讀是一個假說:分散在兩個計數器上,且在每一個上都接近名義上限運行,掃描器從未觸發其中任何一個。這是符合每分鐘總數的最有力解釋,而不是一次實證;總數無法顯示每一分鐘內送出之間的間隔,而 Cloudflare 也承認,在計數器更新期間,超出的請求可能會通過。548
速率限制能分辨快與慢,針對一個資料中心上的一個位址;它無法分辨是誰在送出。
CSRF 權杖
它檢查什麼。 本站的表單帶著一個隱藏的權杖,渲染在頁面中,由三部分組成:時間戳記、隨機值,以及對前兩者的 HMAC 簽章。只要簽章相符且時間戳記不到一小時,伺服器就接受它。15 工作室網站的表單沒有 CSRF 權杖。25
它攔下了什麼。 10月30日四次送出,全在18:03這一分鐘;11月2日一次也沒有。12
其餘為什麼能通過。 這個權杖是無狀態的。它不綁定工作階段、Cookie 或位址,所以任何抓取頁面的用戶端都能拿到一個有效的權杖,而任何從他人手中拿到權杖的用戶端,在權杖有效期間都能出示它。兩台掃描器確實抓取了網站的頁面;請求日誌顯示的正是如此。檢查能顯示的範圍更窄:兩天的每一次寄送都帶著一個我網站簽發、有效且未過期的權杖,因為處理程式在寄送之前會檢查權杖。1513142416 CSRF 權杖的存在,是為了阻止另一個網站讓訪客的瀏覽器送出我的表單。它從來就不是用來阻止一個自己想送出表單的用戶端,而它也確實沒有阻止。
同一項檢查還有第二個缺陷,而它落在人身上,而不是機器人身上。權杖失敗時,處理程式會拋出一個 JSON 403,而 HTMX 表單從不顯示它,所以一個權杖過期的人按下「送出」後,什麼都看不到。12 我修正了這一點,不過我的第一次修正也沒有成功;第6節會說明原因。
有效的 CSRF 權杖證明用戶端持有我網站簽發、未過期的權杖;它無法證明是誰填寫了表單,甚至無法證明這個用戶端載入過頁面。
應用程式限流器
它檢查什麼。 本站的處理程式位於一個裝飾器之後,這個裝飾器在 Cloudflare KV 中依用戶端位址、依時鐘分鐘計算表單送出次數,附帶記憶體內的備援,超過五次就以429拒絕。17 工作室網站針對匿名用戶端有一個每個行程各自的記憶體內限流器,依位址以及用戶端分類計數,所謂分類是限流器依 user agent 與位址指派的類別,涵蓋健康檢查與靜態檔案以外的每一條路徑。25
它攔下了什麼。 10月30日830次送出,11月2日31次。在工作室網站上一次也沒有。127
其餘為什麼能通過。 依設計,它對任何人每分鐘放行五次,而且因為時間窗是時鐘分鐘,一陣跨越分鐘邊界的突發流量會得到兩份額度。17 這些我是知道的。我不知道、直到把計數對照程式碼讀過才發現的是,它會漏,而且程式碼至少有三種漏法。限流器透過 HTTP 從 KV 讀取計數器,與上限比較,然後在另一個請求中寫入計數加一,所以兩次送出若都在任一方寫入之前讀取,就都會通過。遞增操作在寫入之前會第二次讀取計數器,如果這第二次讀取失敗,寫入會把計數設回一,於是這一分鐘重新開始計算。而每當 KV 讀取沒有回傳任何東西或寫入失敗,判斷就退回到保存在單一行程記憶體中的清單,其他行程並不共用這份清單。17
計數顯示了這個漏洞。10月30日,有八分鐘的寄送超過五次,18:06這一分鐘多達18次。1 在掃描器送出的那些分鐘裡,一個每分鐘五次的精確限流器最多會允許54次寄送,而實際寄出了101次,超出上限47次。11月2日,19:02與19:03這兩分鐘放行了9次和10次;一個精確的限流器在它看到的62次送出中最多會允許22次,而實際寄出了31次,超出上限9次。3 計數無法顯示是哪條路徑漏的。我的解讀是:多次送出在任一方寫入之前讀到了相同的計數,或是 KV 呼叫在負載下失敗,導致計數被重設或判斷交給各行程的清單,或是這些情況的某種組合;我手上的日誌記錄的是應用程式回傳的狀態,而不是哪條路徑做出了判斷,所以無法區分這些路徑。173 對所有路徑而言,補救方法都一樣,而且不是改一行程式碼就能解決:一個支援原子遞增的計數器儲存,而先讀 KV 再另外寫入的做法無法提供這一點。這個漏洞是我的錯誤。它也讓本文的論點比我刻意安排還要鮮明:一個說五次卻放行十八次的控制機制,從來就不會是決定誰能寫信給我的那個東西。
在工作室網站上,應用程式的限流器沒有拒絕任何一次觀察到的表單送出,而最忙的位址在一小時內送出了13次。它的計數器會把一個位址在同一用戶端分類下送往其他所有路徑的請求一併算入,而我的資料只涵蓋表單送出,所以這些計數無法衡量那些計數器還剩多少空間。725
當請求輪流到達時,限流器能分辨五和六;不論在什麼數量下,它都無法分辨人和腳本。
蜜罐
它檢查什麼。 工作室網站的表單有一個名為 website 的文字輸入欄位,人永遠看不到它:它帶有 tabindex="-1" 與 aria-hidden,所以鍵盤和螢幕閱讀器都不會停在它上面。如果它回傳時裡面有任何內容,處理程式就回應和人會看到的一模一樣的成功訊息,但不儲存也不寄出任何東西。25 自從我在6月5日建立這份表單以來,這個分支就一直在處理程式中。25 本站的表單沒有蜜罐。24
Ned Batchelder 在2007年描述過這項技術:「These fields are called honeypots and are validated when the form data is posted. If they contain any text, then the submitter must be a bot」(這些欄位稱為蜜罐,在表單資料送出時進行驗證。如果其中含有任何文字,提交者就必定是機器人)。49 他認為這種做法有效的理由是,自動填表的機器人「cannot distinguish invisible fields from visible ones.」(無法分辨隱形欄位與可見欄位。)49
它攔下了什麼。 八天內52次送出中的42次,即80.8%,其中依抵達的小時判斷,包括每一次來自 Tor 出口節點的送出;依小時與訊息模式判斷,也包括一個自稱 SQL 爬蟲的程式在一陣突發中送出的全部13次。720
其餘為什麼能通過。 只填可見欄位的機器人永遠不會碰到 website,而那些送出被儲存、因此可能抵達我收件匣的寄件者,都讓它保持空白。825 說它們是刻意選擇只填可見欄位,是我的解讀;處理程式只記錄隱藏欄位回傳時是空的。Batchelder 早就預見了這一點:「In theory, it is possible for a spambot to defeat any of these measures. But in practice, bots are very stupid」(理論上,垃圾訊息機器人有可能擊敗這些措施中的任何一項。但實際上,機器人非常愚蠢)。49 有些機器人沒那麼蠢,而推銷潛在客戶開發的那些最不蠢。
蜜罐只能證明用戶端填寫了一個人看不到的欄位;空的蜜罐什麼也證明不了。
每條串流走了多遠:兩台掃描器在本站的送出,以及工作室網站上八天的滴流。1278
這五層的共同點
把這五個句子放在一起讀,缺口就很明顯了。速率限制為每個位址的流量設下界線。CSRF 權杖證明用戶端持有網站簽發的權杖。蜜罐證明用戶端填寫了隱藏欄位。edge 規則認得名稱和清單,而唯一能詢問另一端是否有人的那條規則,也就是 user-agent 質詢,只詢問 user-agent 字串在其清單上的請求;它旁邊的威脅分數質詢則永遠不可能觸發。兩台掃描器都沒有被比對到,所以沒有任何東西對每一筆提交提出這個問題,而掃描器做的事,沒有一件是瀏覽器做不到的:它們抓取頁面、保留它的權杖,並以限制只部分放行的速率送出。OWASP 的撞庫攻擊(credential stuffing)指引針對另一種表單提出相同的觀點,建議採用「not rely on a single predictable volume limit」(不依賴單一可預測流量上限)的防禦。50
5. 工作室網站上的緩慢滴流
工作室網站的表單從未遭遇洪泛。它遇到的是滴流,而滴流是另一種動物。這裡的時間窗,從八天查詢中第一個有表單送出的小時,即10月26日 UTC 17:00,到我儲存資料的時刻,即11月3日 UTC 13:59:50;第一天和最後一天都不完整。7
送出次數對照已儲存的列
在這段時間窗內,表單收到52個 POST,每一個都以200回應。同一段時間窗內,訊息資料表增加了10列。由於蜜罐分支是處理程式中唯一回應200卻不儲存任何東西的路徑,蜜罐攔下了其餘42次,即80.8%。7825 依 UTC 日期:78
| 日期(UTC) | 送出次數(200) | 已儲存的列 | 被蜜罐攔下 |
|---|---|---|---|
| 10月26日(不完整) | 4 | 2 | 2 |
| 10月27日 | 5 | 0 | 5 |
| 10月28日 | 3 | 2 | 1 |
| 10月29日 | 5 | 1 | 4 |
| 10月30日 | 16 | 1 | 15 |
| 10月31日 | 8 | 2 | 6 |
| 11月1日 | 4 | 0 | 4 |
| 11月2日 | 5 | 0 | 5 |
| 11月3日(不完整) | 2 | 2 | 0 |
10月30日的高峰來自英國的一個位址,它在單一小時內以 user agent Mozilla/5.0 (compatible; SQLSpider/1.5) 送出了13次。依小時與訊息模式判斷,資料表一則也沒有儲存:那個小時唯一儲存的一列是一則潛在客戶開發推銷,屬於同一小時也有送出的那個美國位址的模式。7820
誰在送出
Cloudflare 會為來自 Tor 網路的請求標上一個特殊的國家代碼 T1。它的標頭參考寫道「T1 - Used for clients using the Tor network」(T1:用於使用 Tor 網路的用戶端),而它的 IP Access rules 頁面寫道「T1: Tor exit nodes (country name: Tor)」(T1:Tor 出口節點,國家名稱:Tor)。5152 依這個代碼,52次送出中有23次來自 Tor,即44.2%,而35個不同位址中有23個是 Tor 出口節點,即65.7%。7 作為交叉檢查,我把每一個送出表單的位址拿去比對 Tor Project 的批次出口清單,該專案稱這份清單為「a nearly real time authoritative source for Tor exits that allow contacting your server on port 80」(一個近乎即時、具權威性的來源,列出允許經由連接埠80連線到您伺服器的 Tor 出口節點)。Cloudflare 標為 T1 的23個位址全都在我11月3日抓取的清單上,其他12個則一個也不在。5354 兩個來源彼此一致。不過那份清單是11月3日的出口節點,所以它確認的是那一天的代碼,並不能證明每次送出抵達的那一天哪些位址是出口節點。該專案的 CollecTor 封存保留了過去的出口清單,可以釐清事件當天的成員身分;我沒有查詢它。55
這段時間窗內大多數位址只送出一次:35個中的32個,即91.4%,其中包括每一個 Tor 出口節點。7 送出超過一次的三個位址,是送出13次的 SQL 爬蟲、一個在一小時內送出兩次的波蘭位址,以及一個送出五次的美國位址。最後這個位址的五次送出,落在資料表在這段時間窗內儲存的五則潛在客戶開發推銷所在的同樣五個小時裡,而且它每次都更換 user agent:先是 Linux 上的 Chrome 135,接著是 Firefox 128、137、138,再回到137。720
user agent 也沿著同一條線分開。在 Tor 送出中,18次標示為 Android 裝置,5次為桌面版 Chrome,23次中有21次標示 Chrome 81或更舊的版本。非 Tor 送出標示的則是比那新得多的桌面瀏覽器,10次是125到135版的桌面版 Chrome,6次是128到138版的 Firefox,外加那個爬蟲以自己名義送出的13次。7
誰被儲存了
兩半在蜜罐那裡交會。在這段時間窗內,沒有任何一列已儲存的資料落在只有 Tor 送出的小時裡,而全部10列都落在有非 Tor 送出的小時裡。23次 Tor 送出中有22次落在完全沒有儲存任何一列的小時裡,第23次則與那個美國潛在客戶開發位址共享同一個小時。20 我以小時來比對,是因為請求日誌與訊息資料表沒有共同的請求 ID,所以這種比對是一種推論:依抵達的小時判斷,這段時間窗內的每一筆 Tor 提交都觸發了蜜罐。20 被儲存、因此可能抵達收件匣的,來自 Tor 之外的位址,它們送出較新的桌面瀏覽器字串,並讓蜜罐保持空白。
這些數字符合表單垃圾訊息的老模式。Batchelder 在2007年的分類,把垃圾訊息的製造者分成「Playback spambots, form-filling spambots, and humans.」(重播型垃圾訊息機器人、填表型垃圾訊息機器人,以及人類。)49 Tor 滴流的行為就像會填滿每個欄位、掉進蜜罐的填表程式。成功穿過的寄件者,行為更接近坐在鍵盤前的人,或是刻意打造得像人的腳本。OWASP 把這種活動歸在 OAT-017,Spamming(垃圾訊息)之下:「Malicious or questionable information addition to public or private content, or messages.」(在公開或私人的內容或訊息中加入惡意或可疑的資訊。)56 OWASP 也排除了一件事:「The mass abuse of broken form-to-email and form-to-SMS functions, to send messages to unintended recipients, is not included in this threat event」(大量濫用有缺陷的表單轉電子郵件與表單轉簡訊功能,把訊息寄給非預期的收件人,不包含在此威脅事件中)。56 我的表單只寄信給我,所以它們屬於被涵蓋的情況,也就是寄給網站擁有者的垃圾訊息,而不是被排除的那種。
滴流在設計上也讓速率限制失效。OWASP 的撞庫攻擊指引點出了把請求分散到許多位址的效果:「This may defeat both IP block-lists and rate limiting, as per IP request volume may remain relatively low」(這可能讓 IP 封鎖清單和速率限制同時失效,因為每個 IP 的請求量可能維持在相對低的水準)。50 每個位址一次送出,已經是流量所能達到的最低程度。
儲存的訊息說了什麼
訊息資料表可追溯到6月6日,我讀取時共有108列;最新的一列在11月3日 UTC 07:24 抵達。8 我用固定規則依模式分類,先符合者優先:57
| 模式 | 整個資料表 | 10月 |
|---|---|---|
| 「Hi, I wanted to know your price」(你好,我想知道你的價格),每列一種語言,同一個寄件者名稱 | 49 | 10 |
| 廠商推銷:網站潛在客戶、Instagram 成長、說明影片、SEO、B2B 資料、目錄網站、設計 | 22 | 4 |
| 獎品誘餌:頭獎、優惠碼、PlayStation、Lamborghini、Rolls-Royce 連結,同一個寄件者名稱 | 16 | 3 |
| telegra.ph 上的「$50,000,000 per month」(每月50,000,000美元)連結 | 9 | 9 |
| 潛在客戶開發推銷:「If I were a real customer, how soon would I get a call back?」(如果我是真正的客戶,多快會接到回電?) | 6 | 5 |
| 其他:兩串亂碼、一則郵寄清單請求、一則要我改寫信到別處的留言 | 4 | 1 |
| 我自己對電子郵件通知程式的測試,9月21日 | 1 | 0 |
| 依我的解讀,一則真正的詢問 | 1 | 0 |
其中幾項值得細看。那九列 $50,000,000 都使用同一個寄件者名稱、九個不同的寄件者位址,每一則都是同一個標題的小變化。8 價格問題在整個資料表中以25種語言出現:南非語、亞美尼亞語、巴斯克語、白俄羅斯語、孟加拉語、保加利亞語、加泰隆尼亞語、克羅埃西亞語、丹麥語、英語、喬治亞語、希臘語、匈牙利語、冰島語、印尼語、愛爾蘭語、義大利語、拉丁語、拉脫維亞語、立陶宛語、盧森堡語、西班牙語、越南語、威爾斯語,以及祖魯語。光是10月就出現了九種:亞美尼亞語、巴斯克語、丹麥語、印尼語、拉丁語、拉脫維亞語、立陶宛語、西班牙語,以及越南語。57 其中兩個語言判定是我做的,而且很接近:我讀成克羅埃西亞語的句子可能是波士尼亞語,我讀成印尼語的可能是馬來語。57 有人正拿著一本常用語手冊,一次一種語言地走過一個聯絡表單。
速率是滴流的另一個特徵。10月共儲存了32列,平均每天1.03列,分布在31天中的21天,單日從未超過三列。8 沒有任何依位址的速率限制會在一天一次的頻率下觸發。
哪些抵達了收件匣
這份表單只從9月21日 UTC 17:46:47 起才會寄信到工作室的收件匣。自那時起,它儲存並因此寄出了43列,這是上限,因為通知呼叫是盡力而為的,而且我沒有核實部署時間;在那個日期之前儲存的65列從未寄出。258 證據時間窗中的每一列都是垃圾訊息。57
除了我自己的測試之外,整個資料表中只有一列不是。9月14日,工作室某項產品的一位客戶寫信來詢問一個具體的錯誤,用的是一個人的口吻,而這則訊息就躺在資料表裡,沒有人讀。57 一週後,當我把表單接上寄信到收件匣的功能時,提交訊息用平白的話說明了原因:「a paying customer’s message sat unread in D1 for a week」(一位付費客戶的訊息在 D1 裡躺了一週沒人讀)。25 那則訊息就是通知程式存在的原因,也是我不能靠關掉通知程式來解決垃圾訊息的原因。我在這裡不透露那位寄件者的身分;重點是,一個真實的人和那本常用語手冊在同一張資料表裡。
6. 機器人檢查是什麼,以及上線了什麼
Turnstile,用 Cloudflare 自己的話說
Cloudflare 稱 Turnstile 為它的「smart CAPTCHA alternative」(智慧型 CAPTCHA 替代方案),並說它「can be embedded into any website without sending traffic through Cloudflare and works without showing visitors a CAPTCHA.」(可以嵌入任何網站,無需讓流量經過 Cloudflare,運作時也不必向訪客顯示 CAPTCHA。)21 機制才是回答我問題的部分。概述寫道:「First, we run a series of small non-interactive JavaScript challenges to gather signals about the visitor or browser environment」(首先,我們執行一系列小型、非互動式的 JavaScript 質詢,以收集關於訪客或瀏覽器環境的訊號),以及「These challenges include proof-of-work (computational puzzles), proof-of-space, probing for web APIs, and various other challenges for detecting browser-quirks and human behavior.」(這些質詢包括工作量證明(運算謎題)、空間證明、探測 Web API,以及其他各種用來偵測瀏覽器特性與人類行為的質詢。)21 速率限制問的是有多少。CSRF 權杖問的是用戶端是否持有網站簽發的權杖。Turnstile 問的是另一端是什麼樣的用戶端。我的 user-agent 質詢也能問這個問題,但只針對 user-agent 字串在其清單上的請求,而我的威脅分數質詢永遠不可能觸發;在這裡真正重要的差別,是選擇性觸發的質詢,對上每一筆提交都必須通過的驗證。
這個小工具有三種模式。我用的是受管模式,Cloudflare 對它的描述是「Automatically chooses between non-interactive or checkbox challenge based on visitor risk level. No images or text to decipher.」(依訪客的風險等級,自動在非互動式質詢與核取方塊質詢之間選擇。沒有需要辨認的圖片或文字。)58 說白了,訪客會看到一個短暫的檢查,除非訊號需要更多,否則就只有這樣;需要更多時,則會出現一個要勾選的方塊。隱形模式連這個都藏起來,而 Cloudflare 為它設了條件:啟用它的網站必須在自己的隱私權政策中引用 Cloudflare 的 Turnstile 隱私權附錄。58 Turnstile 有自己的免費方案,最多20個小工具,質詢次數不限,Cloudflare 並說它「can be used independently without requiring other Cloudflare services.」(可以獨立使用,不需要其他 Cloudflare 服務。)59
伺服器那一半,也就是真正算數的那一半
光有小工具,對表單毫無作用,而 Cloudflare 在它伺服器端指南的開頭幾行就這麼說:「You must call the Siteverify API to complete your Turnstile implementation. The client-side widget alone does not protect your forms.」(您必須呼叫 Siteverify API 才能完成 Turnstile 的實作。只有用戶端小工具並不能保護您的表單。)接著更直白地說:「Tokens can be forged. An attacker can submit any string to your form endpoint without completing a challenge.」(權杖可以偽造。攻擊者可以在不完成質詢的情況下,向您的表單端點提交任何字串。)60 伺服器用它的秘密金鑰把權杖 POST 到 https://challenges.cloudflare.com/turnstile/v0/siteverify,並可以把訪客的 IP 位址作為 remoteip 一併送出。60
權杖的兩項特性決定了圍繞它的程式碼。「Each token is valid for 300 seconds (5 minutes) after generation.」(每個權杖在產生後300秒(5分鐘)內有效。)以及「Tokens are single-use. Each token can only be validated once. A replayed token will be rejected with the timeout-or-duplicate error code.」(權杖只能使用一次。每個權杖只能驗證一次。重播的權杖會以 timeout-or-duplicate 錯誤代碼被拒絕。)60 一台收集到一個權杖的掃描器,無法用它送出表單1,065次。Cloudflare 也把權杖長度上限定為2,048個字元,並告訴伺服器要「Validate the action and hostname when specified.」(在有指定時驗證 action 與主機名稱。)60 同一頁還有一個需要留意的地方:它在一處把 timeout-or-duplicate 用於過期的權杖,卻在錯誤代碼表中把 invalid-input-response 描述為「Token is invalid, malformed, or expired」(權杖無效、格式錯誤或已過期),所以伺服器應該把任何失敗都當成拒絕,而不是把邏輯綁在某一個代碼上。60
它處理什麼,用 Cloudflare 自己的話說
Cloudflare 並沒有宣稱 Turnstile 不使用任何個人資料,我也不會這樣宣稱。它的 Turnstile 隱私權附錄列出了這些訊號:「Cloudflare Turnstile processes a variety of client-side signals (“Signals”) such as client IP address, TLS Fingerprint, User-Agent Header and Sitekey and associated origin.」(Cloudflare Turnstile 處理各種用戶端訊號(「訊號」),例如用戶端 IP 位址、TLS 指紋、User-Agent 標頭,以及 Sitekey 和相關聯的來源。)它說明了目的:「The purpose of collecting these Signals is not to identify, profile or target any individuals but solely to detect and block bots.」(收集這些訊號的目的不在於識別、剖析或鎖定任何個人,而僅在於偵測並阻擋機器人。)它也說 Cloudflare 會用這些訊號來改進產品:「Cloudflare is a data controller of Signals that we process to improve Turnstile’s bot detection capabilities.」(對於我們為改進 Turnstile 機器人偵測能力而處理的訊號,Cloudflare 是資料控制者。)22 Cloudflare 確實表示它不碰的,是內容:「Turnstile does not access, store, or transmit user communications, form entries, or other page inputs.」(Turnstile 不會存取、儲存或傳輸使用者的通訊、表單輸入內容或其他頁面輸入。)21 產品行銷頁面補充說「Turnstile never harvests data for ad retargeting.」(Turnstile 從不為廣告再行銷收集資料。)61 我的解讀是:Turnstile 看到的,是我的 edge 本來就會為每個請求記錄的同類連線資料,而它永遠看不到一個人在我的表單裡輸入了什麼。
它需要的 CSP
Turnstile 從 Cloudflare 的質詢來源載入,而內容安全政策(Content Security Policy)必須允許它。Cloudflare 列出了兩個指令:「script-src: https://challenges.cloudflare.com」與「frame-src: https://challenges.cloudflare.com」。62 我的兩個區域都在一條 Cloudflare 回應標頭規則中設定它們的政策。在本站,frame 清單原本就存在,加入了質詢來源。工作室網站完全沒有 frame-src,所以它的 default-src 'self' 會擋下小工具的 iframe;它加入了 frame-src 'self' 和質詢來源,應用程式在程式碼中自己的政策也加入了同樣的內容。12
為什麼不在表單自己的 POST 上放受管質詢
顯而易見的捷徑早就在我的區域裡了:一條用受管質詢回應表單 POST 的自訂規則。我現有的規則對 curl 及其同類就是這麼做的,而且有效,因為沒有人會用 curl 寄訊息給我。939 把它擴大到每一個 POST,會讓表單對所有人都失效。Cloudflare 直接記載了這個限制:「Challenge Pages interrupt the request flow by returning a full HTML page for the user’s browser to render and solve. This mechanism fails when the browser expects a non-HTML response, such as an AJAX or XHR (fetch) request.」(質詢頁面會中斷請求流程,回傳一整個 HTML 頁面讓使用者的瀏覽器渲染並解題。當瀏覽器預期的是非 HTML 回應時,例如 AJAX 或 XHR (fetch) 請求,這個機制就會失效。)63 本站的聯絡表單是一個 HTMX 表單,以 hx-post 送出,並把回覆換入頁面中,所以它的 POST 正是那一類請求;文件並沒有指名提到 HTMX,這個連結是我的解讀。24 被質詢的 POST 會以一個 HTML 質詢頁面的形式回來,Cloudflare 會用一個「set to challenge」(設為 challenge)的 cf-mitigated 標頭標記它,而且「Regardless of the requested resource-type, the content-type of a challenge will be text/html.」(無論請求的資源類型為何,質詢的 content-type 都會是 text/html。)64 表單會把一個質詢頁面換進它的回應框裡,或者什麼也不顯示。
Cloudflare 對這種情況的答案是另一項功能:「To ensure your API calls are protected without breaking single-page applications (SPAs) or API integrations, Cloudflare recommends using Turnstile Pre-clearance.」(為了確保您的 API 呼叫受到保護,又不會破壞單頁應用程式 (SPA) 或 API 整合,Cloudflare 建議使用 Turnstile Pre-clearance。)63 使用 pre-clearance 時,解開的小工具還會簽發一個 cf_clearance Cookie,它「enables visitors to bypass WAF Challenges on all subsequent requests on the zone, including API requests, based on the security clearance level set by the customer.」(讓訪客能依客戶設定的安全放行等級,在該區域的所有後續請求上略過 WAF 質詢,包括 API 請求。)這個 Cookie 不取代權杖:Cloudflare 說它「is issued to the visitor in addition to the default Turnstile token」(是在預設的 Turnstile 權杖之外另行簽發給訪客的),而同一頁仍然要求伺服器端的檢查:「You must call Siteverify to complete your Turnstile configuration.」(您必須呼叫 Siteverify 才能完成 Turnstile 的設定。)65 pre-clearance 增加的,是一個 XHR 送出能夠撐過的 edge 質詢,前提是訪客的 Cookie 持有等於或高於該質詢等級的有效放行。我用的是預設做法:表單中的一個小工具,以及由我自己的伺服器檢查的權杖。
11月3日上線了什麼
在兩個網站上,我都透過 Cloudflare 的 API 建立了一個受管模式的小工具,範圍限定在該網站自己的網域。每個小工具的秘密金鑰都在同一個流程中,從 API 回應直接進入我 Mac 的鑰匙圈和主機代管服務的變數,從未出現在命令列上或寫入檔案。12
本站。 一個新的驗證程式把權杖連同訪客的位址 POST 到 siteverify。缺少權杖、權杖超過2,048個字元、權杖被拒絕,或傳輸失敗,全都回傳 false,所以只要設定了秘密金鑰,表單就會以關閉狀態失效(fail closed);在沒有設定秘密金鑰的情況下,例如本機開發和測試套件,表單照舊運作。12 Cloudflare 接受的權杖還必須是在本站自己的某個主機名稱上提供的,而當 Cloudflare 回傳小工具的 action 時,它必須是這份表單自己的 action,正如伺服器端指南所建議;沒有主機名稱的回答會被拒絕。因此,在另一個網站上、或由另一個小工具產生的有效權杖都無法通過。這項檢查是同一天早上的第二項變更,從 PST 07:20 開始建置;在 PST 07:25,清除快取之後,一個帶有取自線上頁面之有效 CSRF 權杖、但沒有 Turnstile 權杖的送出,再次回傳403。這證明部署之後表單仍然以關閉狀態失效;一個沒有權杖的送出無法觸及主機名稱檢查本身,那項檢查的依據是程式碼和測試。6660 在第三項變更之前,兩個小工具和兩個驗證程式也都接受 localhost,而由於 sitekey 是公開的,任何人在自己的 localhost 上用我的 sitekey 產生的權杖,都能同時通過 siteverify 和主機名稱檢查。Cloudflare 的測試頁面不建議這樣做:「Cloudflare recommends that sitekeys used in production do not allow local domains」(Cloudflare 建議正式環境使用的 sitekey 不允許本機網域),並把 localhost 列在其中。在 PST 08:12,我把 localhost 從兩個小工具中移除,兩個驗證程式現在預設只接受網站本身和它的 www 主機,所以檢查只接受本站自己的主機名稱,別無其他;本機開發則在一個設定中指定自己的主機名稱。這項變更之後,兩個網站在太平洋時間08:15與08:18再次通過了同樣的線上檢查:頁面上有小工具,而沒有權杖的送出以403被拒絕。沒有任何線上檢查能觸及主機名稱規則本身,因為缺少或無效的權杖在讀取主機名稱之前就已失敗;那條規則的依據是小工具設定、程式碼和測試。4667 處理程式的檢查順序現在是 CSRF,接著 Turnstile,再來才是寄送,前面仍是和之前一樣的依位址限流器。兩種拒絕都以403回傳頁面自己的錯誤片段,而 HTMX 預設不會把4xx回覆換入頁面,所以表單需要一個讓403通過的處理程式。我的第一版處理程式沒有成功。我把它掛在表單上,但 HTMX 是在接收回覆的元素上觸發事件,而在這個頁面上,那個元素是表單的兄弟元素,所以事件從未抵達處理程式,被拒絕的人仍然什麼也看不到。對本文草稿的第二次審查,透過對照範本閱讀打包的 HTMX,抓到了這個問題。修正是那天早上的第四項變更,把監聽器放在頁面 body 上,範圍限定在回應框,並有一項測試斷言它會被渲染。在 PST 08:21,那次部署之後,我在 Playwright 的 Chromium 中執行表單,把小工具的權杖換成一串垃圾字串:回應框顯示「error: Verification failed. Reload the page and try again.」(錯誤:驗證失敗。請重新載入頁面再試一次。),而且沒有寄出任何東西,因為 siteverify 在處理程式走到寄送之前就拒絕了垃圾權杖。68 舊的 CSRF 拒絕,也就是 HTMX 從不顯示的 JSON 403,已經不存在了。表單在每次 HTMX 請求之後重設自己的小工具,並依照 Cloudflare 的重設呼叫所接受的方式指名小工具的容器,所以重試會帶著一個新的權杖,因為每個權杖只能使用一次,壽命為300秒。126069 Cloudflare 的用戶端指南給出了原因和呼叫方式:「To reset the widget if the given widget timed out or expired, you can use the function:」(若要在小工具逾時或過期時重設它,您可以使用這個函式:)turnstile.reset(widgetId);。69 我的第一版在呼叫重設時沒有帶任何引數,而指南並沒有記載這種用法;第四輪審查抓到了這個問題,現在兩個網站上的呼叫都指名了容器,而11月3日的一次瀏覽器檢查在兩份表單上各送出一個垃圾權杖,看到了拒絕,接著在隱藏輸入欄位中找到一個新權杖:在工作室網站上不到八秒,在本站上不到二十五秒,因為本站的小工具先重新執行了質詢。12 小工具的 div 和腳本只有在設定了 site key 時才會渲染。十一項測試通過:沒有秘密金鑰時照舊寄送;有秘密金鑰時,處理程式在任何寄送之前拒絕缺少的權杖,也拒絕被拒絕的權杖,被接受的權杖會寄送,而 CSRF 仍然最先執行;驗證程式接受本站的主機名稱,並拒絕其他主機名稱、預設情況下的 localhost、其他 action,或沒有 action。126646
工作室網站。 蜜罐留在最前面。被填寫的隱藏欄位仍然回應成功訊息、不儲存任何東西,所以最廉價的機器人永遠不會讓我花費一次對 Cloudflare 的呼叫。接著是 Turnstile,缺少、被拒絕或無法驗證的權杖會得到一個附帶錯誤片段的403。然後是驗證、寫入訊息資料表,以及通知。12 它的驗證程式以同樣的方式檢查主機名稱(現在只有網站自己的兩個主機名稱)和 action;這項變更在 PST 07:19 部署,在 PST 07:22,線上頁面帶有小工具和 site key,沒有權杖的送出和帶垃圾權杖的送出都回傳403,而蜜罐送出仍然回應200。66 部署之前,我在本機用 Cloudflare 有文件記載的測試金鑰執行處理程式。使用永遠通過的秘密金鑰時,沒有權杖的送出回傳403,而假權杖通過 Turnstile 並抵達驗證;使用永遠失敗的秘密金鑰時,兩者都回傳403;蜜罐則回應200。1267 Cloudflare 的測試頁面說明了為什麼假權杖在正式環境中無法通過:「Production secret keys: Only accept real tokens, reject dummy tokens.」(正式環境秘密金鑰:只接受真實的權杖,拒絕假權杖。)67
線上檢查。 那天早上稍早,在每個網站各自的部署成功、並清除快取之後,於 PST 06:35,每個首頁都帶著一個小工具 div、一個 Turnstile 腳本標籤和 site key,而每個區域的線上政策都在 frame-src 中列出了質詢來源。對本站表單送出一個帶垃圾 CSRF 權杖的請求,回傳403,但那是 CSRF 檢查在拒絕它,對 Turnstile 什麼也證明不了。Turnstile 檢查本身,我在 PST 06:43 測試:一個帶著取自線上頁面之有效 CSRF 權杖、但沒有 Turnstile 權杖的送出,回傳403,訊息是「error: Verification failed. Reload the page and try again.」(錯誤:驗證失敗。請重新載入頁面再試一次。),而帶著垃圾 Turnstile 權杖的同一個送出也回傳403。所以秘密金鑰已在服務上,而檢查在 CSRF 之後執行。在工作室網站上,沒有權杖的送出和帶垃圾權杖的送出都回傳403,而蜜罐送出回傳200,沒有儲存、也沒有寄出任何東西。70
瀏覽器檢查。 在由 Playwright 驅動的真實 Chromium 中,小工具在兩個頁面上都渲染了,Turnstile 腳本載入了,而隱藏的 cf-turnstile-response 輸入欄位在六秒內於兩個頁面上都持有一個730個字元的權杖。70 這證明的比看起來的少。Cloudflare 的測試頁面警告「Automated testing suites (like Selenium, Cypress, or Playwright) are detected as bots by Turnstile」(自動化測試套件,例如 Selenium、Cypress 或 Playwright,會被 Turnstile 偵測為機器人),而只有 siteverify 能決定一個權杖是否通過。67 這項檢查證明的是,小工具在兩種政策下都能執行並填好它的欄位。我還沒有做的,是對本文讀者最重要的那一項測試:一個人填寫表單並收到回覆。那項測試會寄出一封電子郵件,所以該由我來執行,而我還沒有執行。
7. 速率限制還有什麼用
Turnstile 取代了速率限制作為閘門的角色,但沒有取代它作為圍籬的角色。機器人檢查回答的是「誰」;仍然需要有東西回答「多少」,因為每一筆帶著大小有效之非空權杖抵達檢查的提交,現在都要花費一次對 Cloudflare siteverify API 的呼叫,而每一次通過的送出仍然要花費一次寄送。12 Cloudflare 自己的伺服器端指南把速率限制列入它對 siteverify 呼叫本身的建議中:「Rate limit to protect against validation flooding.」(以速率限制防範驗證洪泛。)60 我的兩道限制都保留:edge 規則為每個位址、每個資料中心每10秒3次送出,應用程式的限流器為每個位址每分鐘五次送出。917
既然我知道了 edge 規則如何計數,它就值得一個誠實的描述。Cloudflare 把設計說得很清楚:「Cloudflare tracks request rates by maintaining separate counters for each unique combination of values in a rule’s characteristics」(Cloudflare 為規則特性中每一組獨特的值組合維護各自獨立的計數器,以此追蹤請求速率),以及「Cloudflare does not support global rate limiting counters across the entire network.」(Cloudflare 不支援橫跨整個網路的全域速率限制計數器。)6 一個經由兩個資料中心抵達我區域的位址,會得到兩份額度,而11月2日那台掃描器的送出確實經由兩個資料中心抵達;這是我對規則為何保持沉默最有力的解釋,與每分鐘的總數一致,但並未被它們證明。5 再加上 Cloudflare 對精確度的但書,也就是這些規則「are not designed to allow a precise number of requests to reach your origin server」(並非設計為讓精確數量的請求抵達您的來源伺服器),這條規則最好描述為一個名義上的預算,而不是一個經過證明的上限:每個位址、每個資料中心每10秒3次表單送出,名義上相當於每分鐘18次,近似地執行。489 在 Free 方案上,它也是我唯一能擁有的速率限制規則,週期固定為10秒,而且只能依 IP 計數。48
如果您像我一樣,想過讓速率限制本身成為質詢,那麼 Free 方案上的一個細節很重要。Cloudflare 的參數頁面說,在 Free、Pro 與 Business 方案上,帶有質詢動作的速率限制規則一律採用節流,而不是維持一段持續時間,而且「When visitors pass a challenge, their corresponding request counter is set to zero.」(當訪客通過質詢時,其對應的請求計數器會被設為零。)71 對表單 XHR POST 的質詢也會因為第6節所說的原因而失敗。63 所以 edge 規則仍然是封鎖,而質詢則放在小工具裡。
應用程式限流器需要它的漏洞所要求的修正:一個支援原子遞增的計數器儲存,讓任何兩次送出都不會讀到相同的計數,以及一條不會把計數分散到不同行程的失敗路徑。目前的程式碼,也就是先讀 KV 再另外寫入、附帶各行程備援,兩者都無法提供,所以這是換掉儲存方式的變更,而不是一行的修補。173 有了 Turnstile 作為每一筆可能抵達收件匣之提交的附加條件,漏洞放過的送出仍然必須帶著 Cloudflare 接受的權杖,所以漏洞耗費的是流量和金錢,而誰能送出的決定不再取決於計數。限流器是一份預算。一份在一次洪泛中就差了47的預算是一份糟糕的預算,但它從來就不是守衛。
OWASP 的身分驗證指引用它自己的話說出了同樣的分層。它把 CAPTCHA 當作「as a defense-in-depth control to make brute-force attacks more time-consuming and expensive, rather than as a preventative.」(一種縱深防禦控制,讓暴力破解攻擊更耗時、更昂貴,而不是一種預防手段。)72 而它的撞庫攻擊指引指出:「Most tools used for these types of attacks will make direct POST requests to the server and read the responses, but will not download or execute JavaScript」(用於這類攻擊的大多數工具會直接向伺服器發出 POST 請求並讀取回應,但不會下載或執行 JavaScript)。50 我的兩台掃描器確實抓取了網站的頁面,這是它們得以持有有效 CSRF 權杖最可能的方式。一個不執行小工具就送出表單的用戶端,沒有 Turnstile 權杖可送,而兩份表單現在都會拒絕沒有權杖的送出:在本站,一個帶有有效 CSRF 權杖、但沒有 Turnstile 權杖的送出回傳403,工作室網站上沒有權杖的送出也一樣。70
檢查清單
對於一份位於 Cloudflare 後方的聯絡表單,依我現在會採取的順序:
- 在表單上放一道機器人檢查,並在伺服器上驗證每一個權杖。 用 Cloudflare 自己的話說:「The client-side widget alone does not protect your forms.」(只有用戶端小工具並不能保護您的表單。)60 也要檢查回答中的主機名稱和 action,正如同一頁所建議,讓在別處產生的權杖無法通過;並讓正式環境的 sitekey 不包含本機網域,因為「Cloudflare recommends that sitekeys used in production do not allow local domains」(Cloudflare 建議正式環境使用的 sitekey 不允許本機網域)。606667
- 以關閉狀態失效。 缺少、過長、被拒絕或無法驗證的權杖都是拒絕,因為「Tokens can be forged.」(權杖可以偽造。)6012
- 每次提交之後重設小工具。 權杖只能使用一次,有效期為300秒,所以不重設就重試,等於拿一個用過的權杖重試。6069
- 在兩個 CSP 指令中都允許質詢來源。 Cloudflare 要求對
https://challenges.cloudflare.com設定script-src和frame-src,而只有default-src 'self'的政策會擋下 iframe。6212 - 不要在以 XHR 或 fetch 送出的表單上放受管質詢。 Cloudflare 說質詢頁面「fails when the browser expects a non-HTML response」(在瀏覽器預期非 HTML 回應時會失效),而 HTMX 的送出正是這類請求。63
- 回傳一個人看得到的錯誤。 我的 CSRF 拒絕是一個 HTMX 表單從不顯示的 JSON 403,所以一個權杖過期的人按下「送出」後什麼也看不到;而我的第一次修正在表單上監聽,HTMX 卻是在回覆的目標,也就是表單的兄弟元素上觸發替換事件,所以請在瀏覽器中測試拒絕的情況。1268
- 讓蜜罐留在機器人檢查前面。 它在我的工作室網站上攔下了52次送出中的42次,代價只是一個隱藏輸入欄位和一個
if。725 - 保留速率限制,並把它定位為成本上限,而不是閘門。 Cloudflare 依位址、依資料中心計數,而11月2日的掃描器經由兩個資料中心抵達,這是它從未觸發最可能的原因,儘管每分鐘的總數無法證明。65
- 用支援原子遞增的儲存來支撐應用程式限流器。 我的限流器讀取計數後在另一個請求中寫回,第二次讀取失敗時把它重設為一,KV 失敗時退回到單一行程的記憶體,並讓寄送超出它自己的上限47次和9次;日誌無法說明是哪條路徑漏的。173
- 不要指望 user-agent 規則擋住掃描器。 兩台掃描器都送出了普通的 Chrome 字串,10月30日至少69,322次,11月2日9,725次。3241
- 在郵件服務那裡計算電子郵件,而不是在表單那裡。 我的處理程式在失敗路徑上回應200,而 Resend 的
email.sent事件只代表「the API request was successful」(API 請求成功);取回的電子郵件的last_event欄位才說明了接下來發生什麼事。242873 - 在一個月內查詢分析資料。 Cloudflare 的自適應資料集保留「at least 31 days of data on every plan」(每種方案都至少保留31天的資料)。27
- 在時間窗結束後重新計數。 我在10月30日洪泛後幾分鐘寫下的筆記少算了表單送出,因為 Cloudflare 還沒有完成匯入最後幾分鐘的資料。16
- 把位址封鎖當成善後。 我的封鎖在10月30日發生在第一次表單送出之後大約50分鐘,在11月2日發生在掃描開始之後8分鐘,而兩天都沒有任何寄送發生在封鎖那一分鐘之後:10月30日有101次在它之前,11月2日有26次在它之前、5次在它之內。1192
可能會改變的情況
這些是條件,不是預測。每一項都說明需要發生什麼,以及我會怎麼應對。
如果有掃描器開始通過受管質詢。 Turnstile 的檢查在瀏覽器中執行,並尋找自動化的跡象;一套能把完整瀏覽器執行得夠好、足以大量通過這些檢查的工具,會讓小工具對它而言只是多一項成本,而不是一道牆。徵兆會是帶著 siteverify 接受之權杖的表單寄送,來自同時在 User-Agent 標頭中送出注入字串的位址。Batchelder 在2007年就為蜜罐寫下了這件事的誠實版本:「If and when they do, we can switch back to CAPTCHAs.」(如果它們真的做到了,我們可以換回 CAPTCHA。)49 我的備案在精神上也一樣,一道更強的質詢,而在那之前,蜜罐和速率限制依然有效。
如果我加入一道帶 pre-clearance 的 edge 檢查。 Cloudflare 建議對無法渲染質詢頁面的請求使用 Turnstile pre-clearance,而 cf_clearance Cookie 讓訪客能「bypass WAF Challenges on all subsequent requests on the zone, including API requests」(在該區域的所有後續請求上略過 WAF 質詢,包括 API 請求)。6563 如果我採用它,edge 就能直接質詢表單的 POST,而不會對 Cookie 持有等於或高於該質詢等級之有效放行的訪客破壞 HTMX;Cloudflare 的 CSP 頁面還為這種模式加了一項要求:「your connect-src directive must include ‘self’.」(您的 connect-src 指令必須包含 ‘self’。)62 這個 Cookie 是「in addition to the default Turnstile token」(在預設的 Turnstile 權杖之外)另行簽發的,而 siteverify 仍然是必要的,所以 edge 質詢會是我伺服器檢查前面的第二道檢查,而不是取代它。65 我還沒有這樣做,本文描述的是目前上線的小工具加 siteverify 設計。
如果我匯出這兩段時間窗的 Resend 日誌。 本文中的每一個電子郵件計數都是上限,因為表單的200代表一次寄送,或是我的處理程式同樣以200回應的失敗。24 Resend 的取回端點會為每封電子郵件回傳一個 last_event,例如 delivered,而它的事件清單區分了「sent - The email was sent successfully.」(sent:電子郵件已成功寄出。)與「delivered - Resend successfully delivered the email to the recipient’s mail server.」(delivered:Resend 已成功把電子郵件遞送到收件人的郵件伺服器。)7374 有了那份匯出,101和31就會變成確切的計數,而且只可能變少。
如果有人手動寄送垃圾訊息。 Turnstile 問的是是否有瀏覽器在場,有時也問是否有人在場。它不問為什麼。一個受雇把推銷文字貼進聯絡表單的人能夠通過它,而如果我工作室網站上那些潛在客戶開發的寄件者背後是人,它們也能通過。對於這種情況,防禦還是老一套:用速率限制控制流量,以及我自己在收件匣裡的判斷。
如果 Tor 滴流需要逐日的答案。 我檢查的出口清單是我在11月3日抓取的那一份,而 Tor Project 的 CollecTor 服務保留了過去的清單。5355 如果我針對這八天查詢它,Tor 的歸屬判斷就會建立在送出抵達當天的清單上,而不是 Cloudflare 的代碼加上11月3日的清單。Cloudflare 也在每種方案上提供 Onion Routing,直接透過 Tor 網路為 Tor 使用者提供服務;Cloudflare 說,有了它,「Human Tor users and bots can be distinguished by our Onion services, such that interactive challenges are only served to malicious bot traffic.」(我們的 Onion 服務能區分人類 Tor 使用者與機器人,使互動式質詢只提供給惡意機器人流量。)75 我還沒有開啟它,而在開啟之前,我會想先量測工作室網站在 Turnstile 之後的 Tor 流量。
關鍵要點
如果您經營一份聯絡表單:
- CSRF 權杖加上速率限制,仍然會讓掃描器寫信給您。我的兩台掃描器都持有有效的權杖,而我的限流器放行了101次和31次寄送。12
- 在伺服器上驗證機器人檢查的權杖,以關閉狀態失效,並在每次請求之後重設小工具;只有小工具「does not protect your forms.」(並不能保護您的表單。)60
- 在負載下和失敗情況下測試您的限流器,而不是一次只送一個請求。一個先讀後寫、附帶各行程備援的計數器,在設定為允許五次的一分鐘內放行了18次寄送。117
- 保留蜜罐。它是整個堆疊中最便宜的過濾器,在我的工作室網站上,八天內在處理程式檢查位址、儲存一列或寄出郵件之前,就攔下了這些提交中的80.8%。725
如果您負責網站的 edge:
- 弄清楚您的速率限制計算的是什麼。在 Cloudflare 上,每條規則都依資料中心計數,而經由兩個資料中心抵達的掃描器會得到兩個計數器。65
- 探測您自己的規則,並在事後閱讀防火牆事件。我的探測產生的十一筆防火牆事件,比一整天推敲規則為何保持沉默,告訴了我更多關於這條規則的事。18
- 不要用質詢頁面去質詢 XHR 或 fetch 的 POST;Cloudflare 記載了這會失敗,並建議在這種情況下使用 pre-clearance。63
- 預期 user-agent 規則只能抓到誠實的工具。這裡的掃描器送出的是瀏覽器字串;我的 curl 測試則招來了質詢。3239
如果您是讀收件匣的那個人:
- 收件匣塞滿時,先看寄件者,再看主旨。我有三條郵件串流共用同一個寄件者位址,這就是我第一個猜測錯誤的原因。23
- 在郵件服務的日誌證明之前,把表單的200當作電子郵件的上限,而不是計數。2428
- 在關掉通知程式之前,先讀一讀儲存下來的訊息。曾經有一位真正的客戶的訊息躺了一週沒人讀,而那則訊息正是工作室表單會寄信給我的原因。25
常見問題
為什麼我的聯絡表單會穿過 Cloudflare 收到垃圾訊息?
最可能的原因是,表單前面沒有任何東西會對每一筆提交詢問是不是人送出的。Cloudflare 的自訂規則比對的是位址、網路、路徑與 user-agent 字串,而它的速率限制是依位址、依資料中心計算請求;一台送出瀏覽器 user agent、先載入頁面、再以中等速率送出的掃描器,能通過所有這些機制。在我的網站上,兩台掃描器正是這麼做的,分別讓101筆和31筆提交通過;而在我量測的八天裡,我工作室網站的垃圾訊息大多是每個位址一次送出,這是任何依位址的計數都抓不到的。96127
速率限制能擋住機器人嗎?
速率限制為單一位址能有多快設下界線;它無法分辨機器人和人。Cloudflare 自己的文件說它的規則「are not designed to allow a precise number of requests to reach your origin server」(並非設計為讓精確數量的請求抵達您的來源伺服器),而且計數器不會在資料中心之間共用。我的 edge 規則對兩台掃描器都沒有觸發,而我設定為每分鐘五次的應用程式限流器,在一分鐘內放行了18次:它的計數器是先讀取、再另外寫入,第二次讀取失敗會把它重設為一,而儲存失敗時會由各行程的備援接手;這些路徑中的任何一條都可能放行額外的送出。48641173
蜜罐欄位就夠了嗎?
對會填寫隱藏欄位的機器人來說是夠的,而在我的工作室網站上,那占了這些提交的80.8%:我的蜜罐在八天內攔下了52次送出中的42次,其中依抵達的小時判斷,包括每一次來自 Tor 出口節點的送出。對讓隱藏欄位保持空白的寄件者,它毫無作用,而它放行的那10筆,被儲存、因此最多10封電子郵件,正是這麼做的。720 Ned Batchelder 在2007年描述這項技術時,也說過同樣的話:「In theory, it is possible for a spambot to defeat any of these measures.」(理論上,垃圾訊息機器人有可能擊敗這些措施中的任何一項。)49
Turnstile 與 CAPTCHA:有什麼差別?
傳統的 CAPTCHA 要求訪客解開一道謎題。Turnstile 執行「a series of small non-interactive JavaScript challenges to gather signals about the visitor or browser environment」(一系列小型、非互動式的 JavaScript 質詢,以收集關於訪客或瀏覽器環境的訊號),而在受管模式下,它只在訪客的風險需要時才顯示核取方塊,「No images or text to decipher.」(沒有需要辨認的圖片或文字。)2158 Cloudflare 說它的質詢不使用「CAPTCHA puzzles or visual tests like selecting objects or typing distorted characters.」(CAPTCHA 謎題,或選擇物件、輸入扭曲字元之類的視覺測試。)76 兩者都仍然需要伺服器驗證結果;對 Turnstile 而言,那就是 siteverify 呼叫。60
為什麼 CSRF 防護擋不住垃圾訊息?
正確實作的 CSRF 防禦,目的是阻止另一個網站讓訪客的瀏覽器送出您的表單。我的權杖比那還弱:它是無狀態且經過簽章的,有效期一小時,不綁定工作階段、Cookie 或位址,所以它只證明用戶端持有我網站在一小時內簽發的權杖,而不是請求來自哪裡。兩種都擋不住一個載入您的頁面、自己送出表單的用戶端,因為載入頁面正是用戶端取得有效權杖的方式,而兩台掃描器都收集到了有效的權杖;在兩波洪泛中,這項檢查只拒絕了它們的四次送出。1512
如何在日誌中分辨掃描器與人?
看這個位址做過的所有事,而不只是表單。我的兩台掃描器爬過數千條路徑,把 SQL 注入字串放進路徑和 User-Agent 標頭,大多數請求送出 Chrome user agent,部分請求則自稱 Googlebot,而 Cloudflare 的已驗證機器人規則並不接受,並以每分鐘數十到數百次的突發送出表單。31324441 sleep(15) 和 waitfor delay '0:0:15' 之類基於時間的酬載,屬於有文件記載的探測家族。3377 一個人只會送出一則訊息,從一個他真正讀過的頁面送出,而且不會要求您的伺服器睡一下。
Turnstile 會收集個人資料嗎?
Cloudflare 並沒有說它什麼都不收集。它的 Turnstile 隱私權附錄列出了它處理的訊號,「such as client IP address, TLS Fingerprint, User-Agent Header and Sitekey and associated origin」(例如用戶端 IP 位址、TLS 指紋、User-Agent 標頭,以及 Sitekey 和相關聯的來源),說明目的「solely to detect and block bots」(僅在於偵測並阻擋機器人),並表示為了改進機器人偵測,Cloudflare 是這些訊號的資料控制者。它的概述說 Turnstile「does not access, store, or transmit user communications, form entries, or other page inputs.」(不會存取、儲存或傳輸使用者的通訊、表單輸入內容或其他頁面輸入。)2221 所以它看到的是連線資料,而不是一個人輸入了什麼。
本站相關文章。 Claude Code 的 auto 模式不是安全邊界針對另一種控制機制提出了同樣的論點:一個逐一判斷每個動作的分類器並不是那道牆。Agents.txt不是存取控制劃清了「向機器人宣告政策」與「執行政策」之間的界線。警報疲勞:資安監控程式一天寄出82封電子郵件從我監控程式的那一側講述11月2日的掃描器:在第一筆發現之後八秒、掃描開始之後八分鐘封鎖,而關於它的電子郵件是零封。睡前必跑的那道指令是另一種維運例行工作,每晚檢查15,000個頁面的狀態、回應時間與快取;而無人監督執行 AI 代理程式時,真正會壞掉的是什麼整理了無人監督之代理程式工作階段的失敗模式,也就是管理我基礎設施的那種自動化。
資料來源
作者的資料說明:每一個來自我自身系統的計數,都取自 Cloudflare 針對兩個區域的 GraphQL Analytics API(請求與防火牆事件資料集)、工作室網站的訊息資料表、我監控程式的日誌,以及兩份表單的程式碼,全部於2026年11月3日儲存,並以腳本整理成帳本。除標明 PDT 或 PST 者外,時間皆為 UTC。本文不出現該證據中的任何位址、規則或清單識別碼、金鑰或檔案系統路徑。
-
作者的資料。Cloudflare 針對本站區域的請求分析,10月30日掃描器對聯絡表單的 POST,依分鐘與 edge 狀態,於2026年11月3日針對已結束的時間窗查詢:1,065次送出,其中101次回應200,830次回應429,130次回應422,4次回應403,全部抵達應用程式;第一次送出與第一個200在18:00這一分鐘,高峰為18:03這一分鐘的208次送出,單一分鐘最多的200為18次(18:06),有八分鐘的200超過五次,最後一個200在18:36,最後一次抵達應用程式的送出在18:37。帳本中的推導:封鎖前101次寄送,第一次送出比我回報洪泛早約46分鐘、比封鎖早49.88分鐘,最後一次抵達應用程式的送出比封鎖早11.88分鐘。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
作者的資料。Cloudflare 針對本站區域的請求分析,11月2日掃描器對聯絡表單的 POST,依分鐘與 edge 狀態,於2026年11月3日查詢:500次送出,其中31次回應200,31次回應429,438次在封鎖當分鐘或之後回應403;62次抵達應用程式;第一次送出在18:58這一分鐘,抵達應用程式的送出高峰為19:02這一分鐘的32次,單一分鐘最多的200為10次(19:03),最後一個200在19:04這一分鐘;從第一次抵達應用程式的送出(18:58這一分鐘)到最後一次(19:04這一分鐘)的七分鐘跨度內,平均每分鐘8.86次送出;31次寄送中,26次在封鎖那一分鐘之前的幾分鐘,5次在封鎖當分鐘19:04,也就是該位址於19:04:01被列入清單的那一分鐘,而分鐘時段無法將它們與那一秒排定先後;之後沒有任何寄送。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
作者在帳本中的分析:對每一分鐘,取五與應用程式限流器所看到之送出數(200、429,以及應用程式的 CSRF 403;不含422,因為 FastAPI 在限流器執行前就拒絕了它們)兩者中較小者,再將各分鐘加總。10月30日:精確限流器最多允許54次寄送,實測為101次,超出47次,限流器看到的送出共935次。11月2日:最多22次,實測31次,超出9次,看到的送出共62次。這是一個反事實的上限,而非量測值;實測的寄送數是上述的表單計數。 ↩↩↩↩↩↩↩
-
作者的資料。Cloudflare 針對11月2日位址的防火牆事件,於2026年11月3日查詢(抽樣資料集,用於判斷哪條規則、在何時動作,從不用於數量):第一筆事件於18:57:11來自漏洞利用路徑規則,第一筆封鎖清單規則事件於19:04:03;每一列都是封鎖,沒有任何一列是受管質詢;聯絡表單上有310列,每一列都是封鎖,第一列在19:04:05;該位址沒有任何來自速率限制規則的事件。 ↩↩↩↩↩↩↩↩↩↩
-
作者的資料,帳本附錄。Cloudflare 針對兩台掃描器之表單 POST 的請求分析,依 Cloudflare 資料中心、分鐘與狀態分組,於2026年11月3日查詢:11月2日從18:56到查詢終點19:05:00,因此計數只包含19:05這一分鐘的第一次送出,經由阿姆斯特丹44次,經由倫敦41次;在19:02這一分鐘,經由倫敦18次(4次寄出,14次被應用程式拒絕),經由阿姆斯特丹14次(5次寄出,9次被拒絕);單一資料中心在單一分鐘內的最大計數為19,是阿姆斯特丹在19:04這一分鐘的計數,其中15次在封鎖後於 edge 被拒絕。10月30日,全部1,065次送出都經由巴黎。 ↩↩↩↩↩↩↩↩
-
Cloudflare,“How Cloudflare determines the request rate”,Cloudflare WAF 文件,擷取於2026年11月3日 UTC 14:08:57。依特性組合分開的計數器;計數器不在資料中心之間共用,同一地理位置的資料中心除外;
cf.colo.id是必要特性;沒有全域計數器。 ↩↩↩↩↩↩↩ -
作者的資料。Cloudflare 針對工作室網站區域的請求分析,對聯絡表單的 POST,依小時、位址、國家與 user agent,自10月26日 UTC 17:00 至2026年11月3日 UTC 13:59:50:52次送出,全部回應200,來自35個位址;帶有 Cloudflare Tor 國家代碼的送出23次、位址23個(44.2%與65.7%);恰好送出一次的位址32個(91.4%),每一個 Tor 位址都在其中;三個重複送出的位址分別為13次(英國,一小時內,SQLSpider user agent)、2次(波蘭,一小時內)與5次(美國,每次都換新的 user agent);Tor 的 user agent 為18個 Android 與5個桌面版 Chrome,21個標示 Chrome 81或更舊;非 Tor 的 user agent 為10個桌面版 Chrome 125到135、6個 Firefox 128到138,以及13個其他。該區域的防火牆在這段時間窗內沒有對表單的 POST 記錄任何事件。蜜罐觸發數為送出數減去已儲存列數:42,即80.8%。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
作者的資料。工作室網站的聯絡訊息資料表,於2026年11月3日 UTC 14:04 讀取:108列,從6月6日到11月3日 UTC 07:24,其中10列在查詢後的時間窗內;每日數字如第5節的表格;10月共32列,分布在31天中的21天,平均每天1.03列,單日從未超過三列;九列 $50,000,000 使用同一個寄件者名稱、九個寄件者位址;通知程式提交之後43列,之前65列。 ↩↩↩↩↩↩↩↩↩↩↩
-
作者的資料。兩個網站的區域設定,於2026年11月3日透過 Cloudflare 的 API 匯出:本站的速率限制規則(版本1,最後更新於2026-10-30 UTC 18:49:53;運算式如引用;特性為資料中心與用戶端位址;每10秒週期3個請求;10秒緩解逾時,名義上相當於每個位址、每個資料中心每分鐘18次)及其五條自訂規則(一條帳號清單與七個 ASN 的封鎖、一條漏洞利用路徑封鎖、一條已驗證機器人略過,以及兩條針對聯絡表單與管理路徑的受管質詢,分別依 user-agent 字串與威脅分數,匯出時兩者都已啟用,威脅分數那一條於當天稍後停用);工作室網站同類的規則,質詢套用至整個網站,另有一條豁免表單的 API 路徑封鎖,以及一條全站速率限制規則,依資料中心與位址,對非已驗證機器人之用戶端的請求計數。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
作者的資料。Cloudflare 針對10月30日位址的防火牆事件,於2026年11月3日查詢(3,932列抽樣資料,低於查詢上限,僅用於歸屬與時間判斷):第一筆事件於17:59:53來自漏洞利用路徑規則,該規則從掃描的第一分鐘起就有動作;18:49:53之前的111筆抽樣事件全部來自該規則;第一筆封鎖清單規則事件於18:49:53;該位址沒有任何來自速率限制規則的事件,也沒有任何聯絡表單上的事件。 ↩↩↩↩↩↩↩↩↩↩↩
-
Cloudflare,“cf.threat_score”,Ruleset Engine 欄位參考,擷取於2026年11月3日 UTC 16:12:44。該欄位的說明:一個過去介於0到100之間的分數,以及「Now, the threat score is always 0 (zero).」 ↩↩↩
-
作者對2026年11月3日上線內容的紀錄,依工作階段的工具輸出撰寫:兩個小工具(受管模式,約於 UTC 14:13 透過 Cloudflare 的 API 建立)、兩個區域上的 CSP 變更、本站的驗證程式與處理程式順序(以關閉狀態失效,先 CSRF、再 Turnstile、再寄送,403錯誤片段,每次 HTMX 請求後重設小工具)、它的十項新測試(10項通過;第十一項隨主機名稱檢查而加入)、工作室網站的處理程式順序(先蜜罐,再 Turnstile,再驗證、儲存與通知),以及使用 Cloudflare 測試金鑰的本機檢查;在兩個網站上,驗證程式都會拒絕缺少的權杖或超過2,048個字元的權杖,且不呼叫 siteverify(依程式碼判讀)。兩個網站於 UTC 14:32 到14:36 之間部署。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
作者的資料。Cloudflare 針對10月30日位址的請求分析,逐分鐘以及掃過我帳號中每個區域,於2026年11月3日查詢:從17:59這一分鐘到18:51這一分鐘,對本站共76,429個請求(53個時段),兩項查詢總數相同;封鎖那一分鐘之前69,484個,平均每分鐘1,389.7個,高峰為18:36的3,219個,封鎖那一分鐘之後5,234個,全部未被提供;依結果,49,513個已提供(64.8%),18,990個被應用程式拒絕(24.8%,其中10,019個是來自網站代理程式閘門的 GET 418),2,339個來源伺服器520(3.1%),5,587個在 edge 被封鎖(7.3%);從掃描的第一分鐘到封鎖共50.88分鐘;對 kiradex.app 的387個 GET,全部回應200,沒有碰觸其他區域。 ↩↩↩↩↩↩↩↩↩↩
-
作者的資料。Cloudflare 針對11月2日位址的請求分析,依分鐘、路徑、user agent,以及掃過每個區域,於2026年11月3日查詢:UTC 07:49 的兩個 GET,比掃描早11.1小時;從18:56這一分鐘到19:17這一分鐘共10,907個請求(22個時段),加上稍早的 GET 為10,909個,四項查詢總數相同;第一分鐘29個請求,封鎖前最忙的一分鐘(18:57)637個,最忙的一分鐘(19:07)1,107個,最後一分鐘300個;封鎖那一分鐘之前2,986個(平均每分鐘373.2個),之後7,445個,全部未被提供;依結果,1,388個已提供(12.7%),1,473個被應用程式拒絕(13.5%,其中420個 GET 418),116個來源伺服器520(1.1%),7,930個在 edge 被封鎖(72.7%);從掃描的第一分鐘到封鎖共8.02分鐘;對 kiradex.app 的368個 GET,沒有碰觸其他區域。 ↩↩↩↩↩↩↩↩↩↩↩
-
作者的程式碼判讀。本站在10月30日與11月2日部署的 CSRF 模組:權杖由時間戳記、隨機值,以及對兩者的 HMAC-SHA256 簽章組成;簽章相符且時間戳記未滿3,600秒即有效;不綁定工作階段、Cookie 或位址。於2026年11月3日從原始碼讀取。 ↩↩↩↩
-
作者的資料。依儲存的查詢結果以腳本建立的帳本(484列,每列附有來源與方法)及其編碼手冊,2026年11月3日,包括每個狀態由哪一層回應的規則、防火牆事件提供歸屬與時間戳記而請求資料集提供計數的規則、只有200能證明有效 CSRF 權杖的規則,以及我當天的筆記與11月3日查詢之間差異的表格(10月30日的筆記寫於 PDT 11:58,當時 Cloudflare 尚未匯入最後幾分鐘的資料,因此少算了表單送出、422、繪圖分享送出,以及該小時的請求)。 ↩↩↩↩↩↩↩↩
-
作者的程式碼判讀。本站在10月30日與11月2日部署的表單限流器:預設每個用戶端位址每個時間窗五次表單送出,60秒的時間窗以時鐘分鐘為鍵,從 KV 讀取計數器,在遞增中進行第二次讀取並把失敗的讀取算作零,再以另一次 KV 寫入寫入該計數加一,讀到的計數達到上限時回傳429,KV 呼叫失敗時退回到各行程的記憶體內清單。於2026年11月3日從原始碼讀取。 ↩↩↩↩↩↩↩↩↩↩
-
作者的資料,帳本附錄。我在2026年11月3日從自己的電腦對本站速率限制規則進行的探測:UTC 14:36:49,十個不帶權杖、間隔0.4秒的 POST(四個422,接著六個429);UTC 14:38:03,八個所有欄位齊全的 POST(三個來自來源伺服器的403,接著五個內文為「error code: 1015」的429);Cloudflare 從 UTC 14:35 到14:40 針對表單的防火牆事件,於 UTC 14:40:36 擷取:11筆事件,每一筆都是速率限制規則的封鎖。 ↩↩↩↩
-
作者的資料。我監控程式在2026年11月2日 PST 10:40 到11:32 的日誌,以及它的發現與封鎖紀錄,於11月3日儲存:一次於 PST 10:57:00 開始的執行,偵測器時間窗於 UTC 18:57:22 結束,沒有發現,依逐分鐘資料,該位址在那個時間窗內的請求介於29到666個之間;下一次執行於 PST 11:02:46;第一筆發現於 PST 11:03:53(嚴重等級的請求洪泛,該位址計得1,914個請求),在掃描第一分鐘之後7.88分鐘;該位址於 PST 11:04:01 被加入帳號封鎖清單;共六筆發現,每一筆都併入每日摘要,沒有一筆以電子郵件寄出。監控程式在10月30日沒有運作;我在10月31日重新啟用它,而它的流量偵測器是之後才加上的。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
作者的分析。把工作室網站已儲存的列與每小時 POST 資料依小時時段比對,2026年11月3日:沒有任何已儲存的列落在只有 Tor 送出的小時;全部10列都落在有非 Tor 送出的小時;23次 Tor 送出中有22次落在沒有已儲存列的小時,第23次落在與美國潛在客戶開發位址共享的小時;時間窗內五列潛在客戶開發訊息,落在該位址送出的五個小時內。這是依小時的推論,而不是以請求 ID 進行的聯結。 ↩↩↩↩↩↩↩
-
Cloudflare,“Overview”,Turnstile 文件概述,擷取於2026年11月3日 UTC 14:08:57。「Cloudflare’s smart CAPTCHA alternative」;無需 Cloudflare 流量即可嵌入;非互動式 JavaScript 質詢,以及工作量證明與空間證明的描述;Turnstile 不存取、儲存或傳輸表單輸入內容的聲明。 ↩↩↩↩↩↩
-
Cloudflare,“Turnstile Privacy Addendum”,最後更新於2025年6月18日,擷取於2026年11月3日 UTC 14:08:57。Turnstile 處理的訊號、其聲明的目的,以及 Cloudflare 為改進機器人偵測而擔任資料控制者的角色,皆為引文。 ↩↩↩
-
作者的營運日誌(Captain’s Log),2026年11月2日:我向管理我基礎設施的代理程式工作階段提出的問題,也就是是不是它在寄這些電子郵件;共用同一個寄件者位址的三條郵件串流;帶著掃描器酬載的電子郵件「Contact Form:」主旨;以及我記下掃描器的表單送出「ran about nine a minute」(每分鐘大約九次)的筆記。 ↩↩↩↩↩
-
作者的程式碼判讀。本站在10月30日與11月2日部署的聯絡表單處理程式與表單範本:一個 HTMX 表單(
hx-post送往表單路徑),渲染時帶入 CSRF 權杖;限流器裝飾器位於處理程式之前;CSRF 檢查在寄送之前,失敗時拋出403;透過 Resend 寄送,主旨為「Contact Form:」加上寄件者的姓名;寄送被接受時回傳成功片段;以及兩條失敗路徑,也就是缺少郵件設定,以及寄送失敗或沒有 id,它們以 FastAPI 的預設狀態200回傳錯誤片段。沒有蜜罐欄位。於2026年11月3日從原始碼讀取。 ↩↩↩↩↩↩↩↩↩ -
作者對工作室網站在2026年11月3日之前所部署版本的程式碼判讀與儲存庫歷史:聯絡表單處理程式的蜜罐分支(非空的隱藏
website欄位會回應成功片段,不儲存也不寄出任何東西;它是處理程式的第一項檢查,位於位址檢查、寫入與通知程式之前),自2026年6月5日建立表單的那次提交起就存在;隱藏輸入欄位的tabindex="-1"與aria-hidden;沒有 CSRF 權杖;盡力而為的通知程式,於2026年9月21日 UTC 17:46:47 在一次提交中加入,其提交訊息寫著「a paying customer’s message sat unread in D1 for a week」;以及一個針對匿名用戶端、各行程各自的記憶體內速率限流器,以用戶端位址與用戶端分類(限流器依 user agent 與位址指派的機器人層級)為鍵,涵蓋健康檢查與靜態檔案以外的每一條路徑。於2026年11月3日從儲存庫讀取。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
作者的營運日誌(Captain’s Log),2026年10月30日,寫於 PDT 11:58:我在 PDT 11:46 回報洪泛、在 PDT 11:48 到11:49 進行手動封鎖並建立速率限制規則,以及我在 PDT 11:52 的測試,皆為引文。它的計數是在 Cloudflare 仍在匯入洪泛最後幾分鐘資料時取得的,本文改用11月3日的查詢。 ↩↩↩
-
Cloudflare,“GraphQL API - Limits”,GraphQL Analytics API 文件,擷取於2026年11月3日 UTC 14:08:57。自適應資料集「retain at least 31 days of data on every plan」(每種方案都至少保留31天的資料)。 ↩↩
-
Resend,“Event Types”,Resend webhook 文件,擷取於2026年11月3日 UTC 14:14:47。
email.sent「Occurs whenever the API request was successful. Resend will attempt to deliver the message to the recipient’s mail server.」(每當 API 請求成功時發生;Resend 會嘗試把訊息遞送到收件人的郵件伺服器。);email.delivered代表成功遞送。 ↩↩↩ -
作者的資料。Cloudflare 針對八天查詢中所有其他送出本站聯絡表單之位址的請求分析,排除兩台掃描器與我的測試位址,於2026年11月3日查詢:三個位址(南韓、日本、印度),各自在一天內恰好有兩個200,分別在10月27日、28日與29日。 ↩
-
RIPE NCC,RIPEstat network-info、whois 與 as-overview,針對10月30日的位址及其 AS,擷取於2026年11月3日 UTC 14:10:34 與14:11:34:AS34534,持有者「BULLIONET FBW NETWORKS SAS」,一筆以國家 FR 登記的配置。 ↩
-
作者的資料。Cloudflare 針對10月30日位址、依路徑、方法與狀態分組的請求分析,於2026年11月3日查詢;查詢回傳了3,000列的上限,涵蓋76,429個請求中的74,125個,所以路徑計數是下限:至少2,881條不同路徑,以及至少2,680個在路徑中帶有 SQL 注入酬載的請求,橫跨至少810條不同路徑;對繪圖分享路徑的320個 POST,全部被拒絕(422或400),八天內沒有任何其他位址送出;第2節引用的分享路徑酬載變體,回應404。 ↩↩↩↩
-
作者的資料。Cloudflare 針對10月30日位址、依 user agent 分組的請求分析,於2026年11月3日查詢;查詢回傳了500列的上限,涵蓋76,429個請求中的76,095個,所以這裡的每一個計數都是下限:至少69,322個請求使用同一個普通的 Windows 版 Chrome 141 字串,至少2,755個自稱 Googlebot,至少3,154個帶有 SQL 注入酬載,分布在至少396個不同字串中。 ↩↩↩↩↩↩
-
PortSwigger Web Security Academy,“SQL injection cheat sheet”,擷取於2026年11月3日 UTC 14:10:34。Oracle(
dbms_pipe.receive_message)、Microsoft(WAITFOR DELAY)、PostgreSQL(pg_sleep)與 MySQL(SLEEP)的時間延遲;條件式的IF(condition, SLEEP(10), 'a')形式;Microsoft 與 MySQL 的SELECT @@version;Oracle 的FROM dual。 ↩↩↩↩↩ -
sqlmap 專案,“time_blind.xml”,該工具基於時間的盲注酬載定義,擷取於2026年11月3日 UTC 14:12:27。包含
SLEEP、PG_SLEEP、WAITFOR DELAY與DBMS_PIPE.RECEIVE_MESSAGE酬載。引用它是為了說明家族,而不是指稱任一掃描器使用了這個工具。 ↩ -
PortSwigger Web Security Academy,“Blind SQL injection”,擷取於2026年11月3日 UTC 14:10:34。為什麼查詢中的時間延遲會延遲 HTTP 回應,引文。 ↩
-
OWASP Automated Threats to Web Applications,“OAT-014 Vulnerability Scanning”,擷取於2026年11月3日 UTC 14:13:41。摘要,引文。 ↩
-
作者的資料,帳本附錄。Cloudflare 針對兩台掃描器之表單 POST、依 edge 狀態與來源伺服器狀態分組的請求分析,於2026年11月3日查詢:10月30日,830個429中有395個帶有來源伺服器狀態429,其餘435個沒有記錄;11月2日,31個429中有16個帶有來源伺服器429,15個沒有記錄。在有記錄來源伺服器狀態的情況下,沒有任何429帶有僅來自 edge 的狀態。 ↩↩
-
作者的測試,2026年11月3日:一個十行的 FastAPI 應用程式,以網站本身的環境(FastAPI 0.128.0)執行,端點上有一個類似限流器的裝飾器。缺少必填欄位的 POST 回傳422,包裝函式從未被呼叫;完整的 POST 則抵達包裝函式。 ↩
-
作者的資料。Cloudflare 針對我自己的測試位址在2026年10月30日的防火牆事件與請求分析,於11月3日查詢:三筆來自速率限制規則的事件,第一筆在 UTC 18:52:38,使用瀏覽器 user agent;以及24筆來自 user-agent 質詢規則、針對我 curl 送出的受管質詢。已從每一個事件計數中排除。 ↩↩↩↩↩
-
RIPE NCC,RIPEstat network-info、whois 與 as-overview,針對11月2日的位址及其 AS,擷取於2026年11月3日 UTC 14:10:34 與14:11:34:AS24875,持有者「NOVOSERVE-AS NovoServe B.V.」,一條登記給 NovoServe B.V. 的路由,國家 NL。子指派的描述指名了一位私人個人,此處不予重述。 ↩
-
作者的資料。Cloudflare 針對11月2日位址、依 user agent 分組的請求分析,於2026年11月3日查詢,資料完整(73個字串涵蓋全部10,909個請求):9,725個請求使用同一個普通的 Windows 版 Chrome 141 字串,942個自稱 Googlebot,54個不同的 SQL 注入酬載分布在149個請求中,3個截斷引號的字串分布在78個請求中,12個
@@標記字串分布在12個請求中,以及2個一般的其他字串。 ↩↩↩↩↩↩ -
作者的資料。兩天的 user-agent 表格,由帳本腳本於2026年11月3日建立:那個普通的 Windows 版 Chrome 141 字串與 Googlebot 字串,在10月30日與11月2日逐位元組完全相同。 ↩
-
作者於2026年11月3日進行的重播,以完整的 Cloudflare 資料,重新執行我監控程式自己的偵測器,涵蓋11月2日 UTC 18:58:23 到19:03:23,也就是 PST 11:02:46 開始的那次執行的時間窗,於 UTC 15:55:04 執行,並儲存為一份附有確切指令與偵測器完整輸出的重播紀錄;帳本附錄中有五列是以腳本從該紀錄推導而來:請求洪泛發現中該位址的2,143個請求、掃描器發現中的1,055個被拒絕請求,以及一筆依據39次送出、其中23次被拒絕的表單洪泛發現,屬於高嚴重等級,會進入每日摘要,既不寄電子郵件也不封鎖。即時執行的1,914,是在一個於執行查詢當下就結束的時間窗內,由我監控程式自己的日誌所記錄,而即時執行沒有提出任何表單洪泛發現。同一天所做的時間窗變更:每個即時時間窗現在在執行前90秒結束,並從上一個時間窗結束的地方開始;當上一筆紀錄遺失或過舊時,則單純回溯五分鐘。 ↩↩↩
-
作者的資料。Cloudflare 針對11月2日位址、依路徑分組的請求分析,於2026年11月3日查詢,資料完整(2,318列涵蓋全部10,909個請求):2,173條不同路徑,以及542條不同路徑上778個帶有 SQL 注入酬載的請求。 ↩↩
-
OWASP Web Security Testing Guide v4.2,“Testing for SQL Injection”,擷取於2026年11月3日 UTC 14:12:12。錯誤型與時間延遲技術,引文。 ↩
-
作者對2026年11月3日第三項變更的紀錄,於審查本文草稿之後進行:UTC 16:12,兩個小工具的允許網域設為網站本身及其 www 主機,移除 localhost;兩個驗證程式的預設主機名稱清單設為同樣兩個主機名稱,本機開發則自行設定清單;本站的測試斷言預設會拒絕 localhost(11項通過);工作室網站重新部署,並在本機以 Cloudflare 永遠通過的測試金鑰執行時,假權杖在列出該金鑰之佔位主機名稱時仍抵達驗證,未列出時則被拒絕。UTC 16:13,以威脅分數為條件的兩條受管質詢規則(每個區域各一條)被停用,原因寫在其描述中;user-agent 質詢保留。這項變更之後的線上檢查:工作室網站於 UTC 16:15(小工具,沒有權杖與帶垃圾權杖的送出回傳403,蜜罐回傳200),本站於 UTC 16:18(小工具,沒有權杖的送出回傳403);沒有一項觸及主機名稱規則。 ↩↩↩
-
Cloudflare,“Custom rules”,Cloudflare WAF 文件,擷取於2026年11月3日 UTC 14:10:34。自訂規則的作用,引文,以及 Free 方案的五條規則,可使用 Log 以外的所有動作。 ↩
-
Cloudflare,“Rate limiting rules”,Cloudflare WAF 文件,擷取於2026年11月3日 UTC 14:08:57。規則參數;精確度的但書與計數器更新延遲,引文;供應情況表格,給予 Free 方案10秒的計數週期、10秒的緩解逾時、依 IP 計數,以及一條規則。 ↩↩↩↩↩↩
-
Ned Batchelder,“Stopping spambots with hashes and honeypots”,建立於2007年1月21日,擷取於2026年11月3日 UTC 14:10:34。蜜罐的定義、機器人為何會上隱形欄位的當、重播型與填表型的分類,以及他關於機器人擊敗這些措施與換回 CAPTCHA 的兩句話,皆為引文。該頁面把這項技術描述為他保護自己網站的方式;它並未宣稱是他發明的。 ↩↩↩↩↩↩
-
OWASP Cheat Sheet Series,“Credential Stuffing Prevention Cheat Sheet”,擷取於2026年11月3日 UTC 14:10:34。不依賴單一可預測的流量上限;每個位址的低流量讓封鎖清單與速率限制失效;直接送出而不執行 JavaScript 的工具,皆為引文。 ↩↩↩
-
Cloudflare,“Cloudflare HTTP headers”,Cloudflare Fundamentals 文件,擷取於2026年11月3日 UTC 14:14:20。CF-IPCountry 標頭的特殊代碼,包括「T1 - Used for clients using the Tor network.」 ↩
-
Cloudflare,“IP Access rules parameters”,Cloudflare WAF 文件,擷取於2026年11月3日 UTC 14:14:20。「T1: Tor exit nodes (country name: Tor)」。 ↩
-
The Tor Project,“Bulk Tor Exit Exporter”,check.torproject.org,擷取於2026年11月3日 UTC 14:10:34。清單的說明,引文,以及指向 CollecTor 取得過去資料的說明;清單本身於同一時間從 https://check.torproject.org/torbulkexitlist 擷取,共有1,239個位址。 ↩↩
-
作者的資料,帳本附錄。在八天查詢中送出工作室網站表單的每一個不同位址,依 Cloudflare 的國家代碼分開,以腳本比對2026年11月3日 UTC 14:10:34 擷取的 Tor Project 批次出口清單(1,239個位址):標為 T1 的23個位址中有23個在清單上,其他12個中有0個。僅限11月3日的清單;事件當天未對照封存檢查。 ↩
-
The Tor Project,“Sources”,Tor Metrics,擷取於2026年11月3日 UTC 14:10:34。TorDNSEL 出口清單「containing the IP addresses of relays that it found when exiting through them」(包含它經由中繼節點出口時所發現之中繼節點的 IP 位址),以及它們的封存。 ↩↩
-
OWASP Automated Threats to Web Applications,“OAT-017 Spamming”,擷取於2026年11月3日 UTC 14:10:34。摘要、包括表單垃圾訊息在內的其他名稱,以及排除針對非預期收件人之表單轉電子郵件濫用的條款,引文。 ↩↩
-
作者的分析。工作室網站的108則已儲存訊息,以固定規則分類,先符合者優先(價格問題與獎品誘餌依寄件者名稱比對,其餘依訊息文字,一列以人工判定為真正的詢問),2026年11月3日:第5節表格中的計數;價格問題在整個資料表中的25種語言與10月的9種,依我對每個固定句子的判讀,克羅埃西亞語(或波士尼亞語)與印尼語(或馬來語)為接近的判定;查詢後時間窗內的每一列都歸類為垃圾訊息。 ↩↩↩↩↩
-
Cloudflare,“Turnstile widgets”,Turnstile 文件,擷取於2026年11月3日 UTC 14:08:57。受管、非互動與隱形模式,以及訪客在每種模式下看到什麼,受管模式為引文;隱形模式的隱私權政策條件;challenges.cloudflare.com 來源。 ↩↩↩
-
Cloudflare,“Cloudflare Turnstile plans”,Turnstile 文件,擷取於2026年11月3日 UTC 14:08:57。Free 與 Enterprise;最多20個小工具;質詢次數不限;無需其他 Cloudflare 服務即可使用,引文。 ↩
-
Cloudflare,“Validate the token”,Turnstile 伺服器端驗證文件,擷取於2026年11月3日 UTC 14:08:57。呼叫 siteverify 的要求;偽造的權杖;端點及其
secret、response與remoteip欄位;300秒的有效期與一次性使用;2,048個字元的上限;action 與主機名稱檢查;錯誤代碼表及其對過期的兩種描述;以速率限制防範驗證洪泛。所有引文段落皆出自此頁。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Cloudflare,“Cloudflare Turnstile - Easy CAPTCHA Alternative”,產品頁面,擷取於2026年11月3日 UTC 14:08:57。「Turnstile never harvests data for ad retargeting.」 ↩
-
Cloudflare,“Content Security Policy”,Turnstile 文件,擷取於2026年11月3日 UTC 14:08:57。對 https://challenges.cloudflare.com 的
script-src與frame-src;以 nonce 為基礎的做法;pre-clearance 的connect-src 'self'要求,引文。 ↩↩↩ -
Cloudflare,“Interstitial Challenge Pages”,Cloudflare Challenges 文件,擷取於2026年11月3日 UTC 14:08:57。質詢頁面如何攔住請求;對大多數 WAF 規則建議使用受管質詢;AJAX 與 XHR (fetch) 的限制,以及對 Turnstile pre-clearance 的建議,兩者皆為引文。 ↩↩↩↩↩↩
-
Cloudflare,“Detect a Challenge Page response”,Cloudflare Challenges 文件,擷取於2026年11月3日 UTC 14:10:34。設為 challenge 的
cf-mitigated標頭,以及每個質詢的 text/html 內容類型,引文。 ↩ -
Cloudflare,“Clearance”,Cloudflare Challenges 文件,擷取於2026年11月3日 UTC 14:10:34。
cf_clearanceCookie 與 pre-clearance,引文:此 Cookie 是在預設的一次性權杖之外另行簽發的,Siteverify 仍然必要,而 Cookie 可略過最高至小工具所設定之放行等級的質詢。 ↩↩↩ -
作者對2026年11月3日第二項變更的紀錄,於同一天早上進行:工作室網站於 UTC 15:19 部署,於 UTC 15:22 進行線上檢查(小工具、腳本、site key,以及線上
frame-src中的質詢來源;沒有權杖的送出與帶垃圾權杖的送出以403被拒絕;蜜罐送出回應200);本站從 UTC 15:20 開始建置,清除快取,於 UTC 15:25 進行線上檢查(同樣的頁面檢查,沒有權杖的送出以403被拒絕,接著一個帶有取自線上頁面之有效 CSRF 權杖、但沒有 Turnstile 權杖的送出再次以403被拒絕,這顯示部署之後表單以關閉狀態失效,但無法觸及主機名稱檢查);兩個驗證程式現在都要求 Cloudflare 回答中的主機名稱是網站自己的主機名稱之一(沒有主機名稱的回答會被拒絕),而當 Cloudflare 回傳 action 時,要求它是小工具自己的 action;本站的十一項測試通過,新的一項接受網站的主機名稱,並拒絕其他主機名稱、其他 action,以及缺少的主機名稱;在工作室網站上,於本機以 Cloudflare 永遠通過的測試金鑰執行時(該金鑰以佔位主機名稱回答且沒有 action),假權杖在列出該主機名稱時抵達驗證,未列出時則被拒絕。 ↩↩↩↩ -
Cloudflare,“Test your Turnstile implementation”,Turnstile 文件,擷取於2026年11月3日 UTC 14:08:57。永遠通過與永遠失敗的測試金鑰以及假權杖;正式環境秘密金鑰拒絕假權杖;自動化測試套件被偵測為機器人,引文。 ↩↩↩↩↩
-
作者對2026年11月3日第四項變更的紀錄,於第二次審查本文草稿之後進行:第一版針對 HTMX before-swap 事件的處理程式,原本要讓403片段越過 HTMX 不替換4xx回覆的預設行為,卻被掛在聯絡表單上,而 HTMX 是在替換目標,也就是表單的兄弟元素上觸發該事件,所以403片段從未顯示;審查透過閱讀打包的 HTMX 與範本發現了這一點。修正是在頁面 body 上設置一個監聽器,範圍限定在回應目標的 id,首頁測試斷言它會被渲染(11項測試通過)。在 UTC 16:18 檢查部署之後,UTC 16:21 在 Playwright Chromium 中進行的瀏覽器檢查,把小工具730個字元的權杖換成一串垃圾字串,以佔位值填入必填欄位,並透過 HTMX 送出表單;回應目標顯示錯誤片段「error: Verification failed. Reload the page and try again.」,沒有成功片段,也沒有寄出任何東西。尚未進行:由人執行的端對端提交。 ↩↩
-
Cloudflare,“Embed the widget”,Turnstile 文件,擷取於2026年11月3日 UTC 14:08:57。api.js 腳本、小工具容器、在表單內建立的隱藏
cf-turnstile-response輸入欄位,以及用於逾時或過期小工具的turnstile.reset(widgetId),引文。 ↩↩↩ -
作者對2026年11月3日線上檢查的紀錄,每項檢查都在其網站自己的部署成功之後,並在快取清除之後:PST 06:35:24 與06:35:54,每個首頁都有一個小工具 div、一個腳本標籤與 site key,每個線上
frame-src都有質詢來源;在本站,一個所有欄位齊全、帶垃圾 CSRF 權杖的送出被 CSRF 檢查以403拒絕;UTC 14:43,在本站,一個帶有取自線上頁面之有效108個字元 CSRF 權杖、但沒有 Turnstile 權杖的送出以403被拒絕,訊息為「Verification failed. Reload the page and try again.」,帶垃圾 Turnstile 權杖的同一個送出也以403被拒絕;在工作室網站上,沒有權杖的送出與帶垃圾權杖的送出以403被拒絕,蜜罐送出回應200,沒有儲存也沒有寄出任何東西;在 Playwright Chromium 中於 UTC 14:39:50 與14:40:04,小工具已渲染(602乘73與557乘71像素),Turnstile API 物件已載入,隱藏的回應輸入欄位在六秒內於兩個頁面上都持有一個730個字元的權杖。尚未進行:由人執行的端對端提交。 ↩↩↩ -
Cloudflare,“Rate limiting parameters”,Cloudflare WAF 文件,擷取於2026年11月3日 UTC 14:08:57。參數名稱;在 Free、Pro 與 Business 方案上,質詢動作一律採用節流,為轉述;通過質詢會重設計數器,引文。 ↩
-
OWASP Cheat Sheet Series,“Authentication Cheat Sheet”,擷取於2026年11月3日 UTC 14:10:34。CAPTCHA 作為縱深防禦控制,引文。 ↩
-
Resend,“Retrieve Sent Email”,Resend API 參考文件,擷取於2026年11月3日 UTC 14:14:47。取回之電子郵件的
last_event欄位,例如「delivered」。 ↩↩ -
Resend,“View and manage emails”,Resend 文件,擷取於2026年11月3日 UTC 14:15:12。電子郵件事件清單,包括「sent」與「delivered」,引文,以及僅適用於 Broadcasts 與 Batches 的「queued」。 ↩
-
Cloudflare,“Onion Routing and Tor support”,Cloudflare Network 文件,擷取於2026年11月3日 UTC 14:08:57。每種方案都提供 Onion Routing,以及人類 Tor 使用者與機器人的區分,引文。 ↩
-
Cloudflare,“Challenges”,Cloudflare Challenges 文件,擷取於2026年11月3日 UTC 14:08:57。「Cloudflare does not use CAPTCHA puzzles or visual tests like selecting objects or typing distorted characters.」 ↩
-
OWASP Community,“Blind SQL Injection”,擷取於2026年11月3日 UTC 14:10:34。依賴資料庫暫停的基於時間盲注,以
WAIT FOR DELAY與pg_sleep()為例。 ↩