スパムがレート制限付きの問い合わせフォームを破った理由

レート制限付きのお問い合わせフォームを、なぜスパムが通り抜けるのでしょうか? レート制限はアドレスごとにリクエストを数えるだけで、誰が送ったのかを問う仕組みを何も持たないからです。10月30日と11月2日、2つの脆弱性スキャナーが私のサイトをクロールし、フォームのあるトップページを普通の訪問者と同じように読み込み、有効なCSRFトークンを手に入れ、立ち去るかブロックされるまでフォームに投稿し続けました。初日は101件、2日目は31件の投稿が通りました。どれもアプリが200を返したもので、アプリは送信のときにも、送信に失敗したときにも200を返します。つまりこの数字は、生じえたメールの最大数です。12 アドレスごとに1分5件に設定した私のアプリのリミッターは、それぞれの日に830件と31件の投稿を拒否し、101件と31件の送信を通しました。その設定の正確なリミッターが許したはずの数より、47件と9件多い数です。3 エッジのレート制限は、どちらのスキャナーにも一度も発動しませんでした。1回目のフラッドでは、最後のフォーム投稿から約12分後に私がそのルールを作ったからです。2回目のスキャナーの投稿は2つのCloudflareデータセンターを経由して届き、データセンターごとにカウンターは別々です。この分割が2回目の沈黙について私が持つ最も有力な説明で、1分ごとの合計と矛盾しませんが、合計でそれを証明することはできません。456 私のスタジオのサイトでは、ハニーポットのフィールドが8日間で52件中42件の送信を捕らえ、送り手がハニーポットを空のままにした10件を通しました。78 このサイトのエッジには、人間がそこにいるかを問えるルール、つまりフォームのパスに対するマネージドチャレンジがありました。しかしそれは一覧にあるユーザーエージェント文字列にしか発動せず、2つのスキャナーはどちらもブラウザーの文字列を送っていました。高い脅威スコアをキーにした2つ目のチャレンジは、Cloudflareが現在そのスコアを常にゼロにしているため、決して発動しえませんでした。こうして、すべての送信にその問いを投げるものは何もありませんでした。910411 11月3日の朝からは、私の受信箱に届きうるすべての送信に対して、両方のフォームでTurnstileがその問いを投げています。12

TL;DR

  • 2つのスキャナー、1つの受信箱。 10月30日、あるアドレスが1分単位の53個のバケットにわたって私のサイトに76,429件のリクエストを送りました。11月2日には別のアドレスが22個のバケットにわたって10,907件を送りました。どちらもサイト全体をクロールし、どちらもパスとUser-Agentヘッダーの両方にSQLインジェクションの文字列を入れ、どちらもお問い合わせフォームに投稿しました。その数は初日が1,065回、2日目が500回です。131412
  • 有効なCSRFトークンが証明するのは、私のサイトが発行したトークンをクライアントが持っていたことだけです。 両日のすべての送信がトークンを持っていました。私のトークンはステートレスなので、ページを取得したクライアントなら誰でも1つ手に入れられるからです。2回のフラッドを通して、CSRFチェックが拒否した投稿は4件でした。1516
  • 私自身のリミッターが漏れていました。 リミッターはCloudflare KVからカウンターを読み、比較し、もう一度読んでから、カウントに1を足した値を別のリクエストで書き込みます。2回目の読み取りに失敗すると1が書き込まれ、KVの呼び出しに失敗すると1つのプロセスのメモリーに保持したカウントにフォールバックします。どの経路でも5件を超えて通すことがありえ、どの経路がそうしたのかはログからはわかりません。上限5件に対して、1分間に18件の送信が出ていきました。171
  • エッジのレート制限は、データセンターごと、アドレスごとに数えます。 11月2日、18:56から19:05:00 UTCまで、つまりブロック前後の数分間のスキャナーのフォーム投稿85件は、アムステルダム(44件)とロンドン(41件)を経由して届き、最も多い1分間は2つの間で18対14に分かれていました。この分割が、ルールが沈黙していた最もありそうな理由ですが、1分ごとの合計でそれを証明することはできません。翌朝に私が行ったプローブで、ルールが発動すること、そしてCloudflareがそれを記録することは証明されました。518
  • ブロックはアドレス単位のブロックで、どちらもメールを止めるには遅すぎました。 10月30日、私は気づいてから3.9分後、そのアドレスの最初のフォーム投稿から約50分後に、手作業でアドレスをブロックしました。11月2日は、スキャンの最初の1分から8分後に私のモニターがブロックしました。どちらの日も、ブロックの入った分より後に出た送信はありません。10月30日は最後の送信がブロックの12分以上前で、11月2日はブロック前の数分間に26件、ブロックの分そのものに5件ありました。後者の5件は、1分単位のデータではブロックの秒より前か後かを決められません。101912
  • ハニーポットは、すべてのフィールドを埋めるボットを捕らえます。 私のスタジオのサイトでは、ハニーポットが52件中42件の投稿に成功したかのように応答し、何も保存しませんでした。到着した時間帯で照合すると、Torの出口ノードからの送信はすべてその中に含まれていました。保存された10件、つまりメールにして最大10通は、ハニーポットを空のままにした送り手からのものでした。720
  • 変えたこと。 両方のフォームに、マネージドモードのCloudflare Turnstileウィジェットを置きました。サーバーは、Cloudflareのsiteverify APIが送信のトークンを受け入れるまで何も保存も送信もせず、トークンがない場合、大きすぎる場合、検証できない場合は拒否します。Turnstileは訪問者のブラウザーでチャレンジを実行し、IPアドレス、TLSフィンガープリント、ユーザーエージェントといったシグナルを処理します。Cloudflareは、フォームの入力内容には触れないとしています。人間が手でスパムを打ち込むことは止められず、量を抑えるレート制限の代わりにもなりません。122122

1. 問い:「それはあなた?」

11月2日、私の受信箱がお問い合わせフォームのメッセージで埋まりました。最初の問いは、私のインフラを動かしているエージェントのセッションに向けたもので、私自身の自動化がそれを送っているのか、というものでした。違いました。送っていたのはスキャナーで、このサイトのお問い合わせフォームを通していました。私のスタジオのサイト941apps.comのフォームも独自に少量ずつの投稿を受けていましたが、その日、受信箱に届いたものはありませんでした。238 モニターのアラートと2つのフォームという3つのメールの流れは、すべて私自身のメールドメインの1つのアドレスから送られるので、受信箱では見分けがつきませんでした。23

件名が正体を明かしていました。このサイトのお問い合わせハンドラーは、送信内容を「Contact Form:」という件名に、送り手が名前の欄に入力したものを続けて私に送ります。そのため、スキャナーのペイロードがそのまま件名になるのです。2423 スタジオのサイトのフォームは、メッセージをデータベースに保存してから、同じ受信箱にメールで送ります。25

最初に投げた問いは間違っていました。正しい問いに答えるには、1日がかりのクエリが必要でした。その問いとは、これらのフォームにある制御のうち、どれがスキャナーと人間を見分けられたのか、そしてそれぞれがなぜ残りを通したのか、です。制御は5種類ありました。このサイトには、カスタムルールを持つエッジのファイアウォール、エッジのレート制限、CSRFトークン、そしてアプリ内のレートリミッター。スタジオのサイトには、同じ種類のエッジのルール、エッジのレート制限、観測されたフォーム投稿を1件も拒否しなかったアプリ独自のリミッター、そしてハニーポットのフィールドです。9257 この記事の残りでは、2回のフラッドと少量ずつの投稿を1つずつ取り上げ、続いて5つの層を1つずつ、それぞれのルールの文面やコードの挙動とともに見ていきます。

数字の出どころ

以下の件数はすべて、2つのゾーンについてのCloudflareのリクエスト分析とファイアウォールイベント(11月3日に、すでに閉じた時間枠に対してクエリしたもの)、スタジオのサイトのメッセージテーブル、そして私のモニター自身のログから来ています。16 スクリプトが保存したクエリ結果を数百行の台帳に変換し、各行にはそれを生んだルールが付いています。この記事の数字は台帳とその補遺からだけ取り、それ以外からは取っていません。当日のメモと台帳が食い違う場合は、台帳を優先します。10月30日のメモはフラッドの数分後、Cloudflareがまだ最後の数分を取り込んでいる最中に書いたもので、数え漏れがあります。1626 Cloudflareはこれらのデータセットを「at least 31 days of data on every plan」(すべてのプランで少なくとも31日分のデータ)保持するので、フラッドを後から数えることはできましたが、いつまでもできるわけではありません。27

2種類の数字には注意が必要です。ファイアウォールイベントはサンプルなので、どのルールがいつ動いたかに使い、件数にはリクエストのデータセットを使います。16 また、このサイトで200が返ったフォーム投稿は、送信か、ハンドラーが同じく200を返す失敗(そうした失敗の経路は2つあります)のどちらかであって、配信されたメールではありません。メールサービス側のログも確認していません。したがって、この記事のメールの件数はすべて上限です。2428

平常時の姿

フラッドの前、フォームは静かでした。クエリが対象とする8日間で、2つのスキャナー以外にフォームへの投稿に成功したアドレスは3つで、どれもちょうど2回ずつ、10月27日、28日、29日に1日1アドレスでした。29 それが人間だったかどうかはわかりません。1つのアドレスからの2回の投稿はダブルクリックとも合いますし、再試行するボットとも合います。この不確かさこそ、問題全体の縮図です。ログも制御も、それを言い当てられなかったのです。

2. 1回目のフラッド:10月30日、手作業でブロック

1回目のフラッドは、地域インターネットレジストリがBULLIONET FBW NETWORKS SASの名義で登録している、フランス登録のネットワーク上の1つのアドレスから来ました。30 最初のリクエストは17:59 UTCの分(太平洋夏時間10:59)に、最後のリクエストは18:51の分(11:51 PDT)に届きました。1分単位のバケット53個で、このサイトへのリクエストは76,429件です。そのアドレスについての1分ごとのクエリと、私のアカウントのすべてのゾーンを横断するクエリという2つの独立したクエリがこの合計で一致し、このアドレスはその前にも後にも何も送っていません。13

スキャンの形

フォームはその一部にすぎませんでした。ブロック前、このアドレスは1分あたり平均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%
エッジでブロック(403) 5,587 7.3%

エッジの7.3%はほぼすべてがブロックによるもので、5,587件の拒否のうち5,234件はブロックの入った分より後のものです。13 それまでの50分間に動いていたのは、エクスプロイトのパスを対象とするルールだけでした。そのルールは最初の1分から機能しており、最初のファイアウォールイベントは17:59:53 UTCです。ただしこのルールは.phpや/wp-のようなパスの部分文字列に一致するもので、このスキャンの大半は実在するページを要求していました。109

これをクロールではなくスキャンにしていたのは、リクエストに何を入れていたかです。トラフィックのほぼすべて、少なくとも69,322件のリクエストが、Windows用のごく普通のChrome 141のユーザーエージェントを1つ使っていました。さらに少なくとも2,755件はGooglebotを名乗っており、エッジの検証済みボットのルールはそれらをGoogleとして扱いませんでした。32 少なくとも3,154件のリクエストはUser-Agentヘッダーそのものに、少なくとも396種類の文字列でSQLインジェクションのペイロードを入れていました。少なくとも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 遅い応答が1回あっても、ほとんど何も証明しません。ペイロードが求めるたびに約15秒の遅延が戻ってきて、ペイロードのない対照リクエストでは戻ってこない。それがインジェクションの可能性の証拠になります。

このサイトのペイントアプリには共有用のエンドポイントがあり、スキャナーはそれを見つけました。共有パスに320件のPOSTを送っています。クエリが対象とする8日間でそこに投稿したアドレスはこれだけで、エンドポイントは320件すべてを拒否し、何も保存しませんでした。その前後には、/paint/share'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||'や/paint/(select 198766*667891 from DUAL)のようにペイロードを組み込んだパスの変種にも投稿し、サイトはそれらに404を返しました。31 このアドレスは私のアカウントのもう1つのゾーンにも触れており、kiradex.appに387件のGETを送ってすべて200を受け取りました。ほかのゾーンには触れていません。13 私の読みでは、このサイトからそのサイトへのリンクをたどったのでしょう。リンクは確認していません。

OWASPの自動化された脅威のカタログには、この活動を表す名前があります。OAT-014、Vulnerability Scanning(脆弱性スキャン)で、「Crawl and fuzz application to identify weaknesses and possible vulnerabilities.」(アプリケーションをクロールしファジングして、弱点と脆弱性の可能性を特定する)とあります。36 お問い合わせフォームは、ファジングする入力がもう1つ増えただけでした。

フォーム、1分ごと

このアドレスはお問い合わせフォームに1,065回投稿し、そのすべてがアプリに届きました。エッジは1件も止めていません。1 最初の投稿と最初の送信は18:00 UTCの分(11:00 PDT)に来ました。以下は、フォーム投稿がアプリに届いたすべての分を、アプリが返したステータス別に示したものです。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件、CSRFチェックによる拒否4件で、この4件はすべて18:03の分です。1 429はアプリが返したものです。エッジのレート制限ルールはまだ存在しておらず、このアドレスについてレート制限のファイアウォールイベントは1件もありません。Cloudflareがこれらの投稿についてオリジンのステータスを記録している範囲では、429はオリジンから来ています。1037 422はFastAPIのフォームバリデーションによるもので、これはリミッターより先に実行されるため、リミッターの枠を消費しませんでした。この順序は10行のFastAPIアプリで確かめました。必須項目を欠いた投稿は、ラッパーを一度も呼ばずに422を返しました。38 18:37の129件のまとまりは、スキャナーがフィールドを落としながらフォームをファジングしていたように見えます。

「送信」の列をリミッターと照らし合わせて読んでください。リミッターはアドレスごと、時計の1分ごとに5件の投稿を許しますが、そのうち8つの分で5件を超える送信が出ており、18:06には18件でした。13 理由は第4節で説明します。

気づき、そしてブロック

私は11:46 PDTに気づき、何かがサイトのお問い合わせフォームを繰り返し叩いている、と書き留めました。26 その時点で最初のフォーム投稿から約46分がたっており、フォームへのフラッド自体は11:37 PDTに終わっていました。1 私はそのアドレスを、ゾーンの最初のカスタムルールがすでに照合しているアカウントレベルのブロックリストに手作業で追加しました。そのルールの最初のイベントは18:49:53 UTC(11:49:53 PDT)です。10 ブロックは、私が気づいてから3.9分後、最初のフォーム投稿から約50分後、スキャンの最初の1分から50.9分後に入りました。113 101件の送信はすべてその前に出ていました。アプリに届いた最後のフォーム投稿は、ブロックの約12分前です。1 ブロックの入った分の後、このアドレスはさらに5,234件のリクエストを送り、エッジはそのすべてを拒否しました。13

同じ秒、18:49:53 UTCに、私はゾーンで唯一のレート制限ルールを作りました。お問い合わせとペイント共有のパスへのフォームのPOSTを対象に、アドレスごとに10秒あたり3件、10秒間ブロックするものです。9 18:52:38 UTCに自分のマシンからそれをテストしました。そのテストについての私のメモには、ブラウザーのユーザーエージェントで有効なCSRFトークンを持たない6件の投稿を1秒間隔で送ると「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拒否、続いてエッジのレート制限))とあり、素のcurlによる投稿は既存のカスタムルールがマネージドチャレンジを返したため、そもそもこのルールに届きませんでした。26 メモには投稿がトークンを持っていなかったとありますが、11月3日の私のプローブが示したとおり、そのフィールドを欠いた投稿はハンドラーが動く前に422になります。したがって403は、トークンが存在していて有効でなかったことを意味します。18 ファイアウォールイベントもこれと一致します。私のテスト用アドレスからのレート制限イベントが3件(最初の1件はその秒)、そして私のcurlの投稿に対するマネージドチャレンジが24件です。39 私のテストのトラフィックは、上記のどの件数にも含まれていません。

その日、私のモニターは動いていませんでした。モニターを再稼働させたのは10月31日で、トラフィックの検出器はこのフラッドの後に加わりました。19 10月30日のブロックは、受信箱を読んだ人間によるものでした。

2026年10月30日、太平洋時間10:59から11:51まで、1つのスキャナーのアドレスがblakecrosley.comに送ったすべてのリクエスト(合計76,429件)を、1分ごとに1本の棒で示し、誰が応答したかで色分けした棒グラフ。ブロック前の棒は1分あたり平均1,390件(ピークは11:36 PDTの3,219件)で、大半は配信されたページ、そこにアプリの拒否と一部のオリジンのエラーが加わる。お問い合わせフォームへのフラッド(1,065件の投稿、うち101件に200が返った。200は送信か、ハンドラーが同じく200を返す失敗なので、メールは最大101通)は、11:00から11:37 PDTまでの38分間に収まる。点線は著者が気づいた11:46 PDTを、破線は彼が手作業で追加したブロックが有効になった11:49:53 PDT(18:49:53 UTC)を示し、スキャンの最初の1分から51分後。それ以降の1分単位のバケットのリクエストは、リクエストのあった最後の分である11:51 PDTまで、すべてエッジでブロックされた。

10月30日:1つのアドレスからの76,429件のリクエスト、1分ごと。私が手作業でアドレスをブロックするまで、エッジはほとんど何も拒否しませんでした。1310

3. 2回目のフラッド:11月2日、私のモニターがブロック

2つ目のアドレスは、オランダのホスティング事業者NovoServe B.V.のものです。40 このアドレスは07:49 UTC(11月1日の太平洋標準時23:49)に2件のGETを送り、その後11.1時間何も送りませんでした。本格的なスキャンは18:56 UTCの分から19:17の分まで(10:56から11:17 PST)続きました。1分単位のバケット22個で10,907件のリクエスト、早朝の2件のGETを含めると10,909件です。4つの独立したクエリがこの合計で一致しています。14

同じツールのように見えました。10月30日のアドレスと同じWindows用Chrome 141のユーザーエージェント文字列を9,725件のリクエストで使い、同じGooglebotの文字列を942件で使い、同じインジェクションのファミリーを送り、同じ2つのゾーンにだけ触れて、kiradex.appには368件のGETを送っていました。414214 2つの国の2つのネットワークにある2つのアドレスでは、誰がそれを動かしたかについて何も証明できないので、「見えた」にとどめておきます。

両方のタイムゾーンでのタイムライン

UTC PST 出来事
18:56 10:56 スキャン開始。この1分間に29件のリクエスト。1419
18:57:00 10:57:00 私のモニターの実行が1回始まる。その時間枠は18:57:22に閉じるので、スキャンを見るのは約1分間だけで、何も見つけない。19
18:57 10:57 ブロック前で最も多い1分間、637件のリクエスト。エッジのエクスプロイトパスのルールが18:57:11にブロックを始める。144
18:58 10:58 最初の3件のフォーム投稿。アプリは3件とも送信する。2
19:00 11:00 さらに4件のフォーム投稿、4件とも送信。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 最後の5件の送信と、エッジで拒否された22件のフォーム投稿。すべてブロックの分そのものの中で、1分単位のバケットではどちらが先かわからない。2
19:07 11:07 スキャンで最も多い1分間、1,107件のリクエスト。すべてブロック。14
19:17 11:17 最後の1分、300件のリクエスト。すべてブロック。14

私のモニターの8分間

私のモニターは、スキャンの最初の1分から7.9分後に最初の検出結果を出し、ブロックはその8.0分後、つまり最初のファイアウォールイベントから6.8分後に有効になりました。194 それまでに、ブロック前の数分間で26件の送信が出ており、最後の5件はブロックの分そのものに入っていました。1分ごとのデータでは、この5件をブロックの秒より前か後かに置くことはできません。それ以降の分には1件もありません。2 8分はブロックとしては速く、受信箱にとっては遅い時間です。

10:57:00 PSTに始まった実行は、スキャンを約1分間だけ視野に入れており、その時間枠の中でこのアドレスは29件から666件のあいだのリクエストを送っていました。上の数字は18:57の1分間をまるごと数えたもので、時間枠が見たのはそのうち22秒だけです。検出器は何の検出結果も記録しませんでした。19 11:02:46 PSTの次の実行がフラッドを見つけてブロックし、その実行は、最初の実行がなぜ見逃しえたのかも示しています。その時間枠はCloudflareに問い合わせた瞬間に終わっており、最新のイベントはまだ届いていませんでした。実行のログにはこのアドレスからのリクエストが1,914件と記録されていますが、11月3日に完全なデータで同じ5分間の時間枠を再生すると2,143件になります。この再生はコマンドと完全な出力とともに保存してあり、その件数は台帳の補遺の行になっています。1943 再生では、ライブの実行が一度も出さなかったフォームフラッドの検出結果も出ました。39件の投稿(うち23件が拒否)による高重大度のもので、メールやブロックではなく、日次ダイジェストの1行になったはずのものです。43 私が再生したのは2回目の実行の時間枠で、1回目のものではありません。したがって10:57 PSTの実行についても同じ遅れが原因である可能性は高いものの、測定したわけではありません。同じ日に、モニターの時間枠を、各実行の90秒前に終わり、前回の時間枠が終わったところから始まるように変更しました。43 このアドレスに対して検出結果は全部で6件発動し、私のモニターはそのすべてをメールで知らせる代わりに日次ダイジェストに畳み込みました。そのアドレスがすでにモニターのブロック台帳に載っていたからです。19 モニターがどうアラートを出すべきか、そしてモニター側から見たこのスキャナーについては、アラート疲れ:セキュリティモニターから1日82通のメールという別の記事のテーマです。この記事にとって大事なのは、ブロックは反応であり、その反応は31件の送信のうち少なくとも26件より後に来た、ということです。

フォーム、ステータス別

このアドレスはフォームに500回投稿しました。2

UTC PST 送信(200) リミッターが拒否(429) エッジでブロック(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件で、エッジでの拒否はすべてブロックの分かそれ以降です。つまり、アプリに届いた投稿は62件です。2 送信の1件1件が、スキャナーが有効なCSRFトークンを持っていたことを証明します。ハンドラーは送信の前にトークンを確認するからです。429はどちらとも証明しません。リミッターは、ハンドラーがトークンを読む前に投稿を拒否するからです。2416 Cloudflareがそれらの429についてオリジンのステータスを記録している範囲では、それはオリジンのもの、つまりアプリのリミッターのもので、このアドレスについてレート制限のファイアウォールイベントは1件もありません。374

スキャンの残り

結果別に見ると、11月2日のスキャンの内訳は次のとおりです。14

結果 リクエスト 割合
配信(2xx、3xx) 1,388 12.7%
アプリが拒否(4xx)、うちエージェントゲートによるもの420 1,473 13.5%
オリジンのエラー(520) 116 1.1%
エッジでブロック(403) 7,930 72.7%

エッジの72.7%は大半がブロックによるものです。ブロックの分より前のリクエストが2,986件、後が7,445件で、サイトは後者を1件も配信していません。14 この日のパスのクエリは完全でした。異なるパスは2,173個で、そのうち542個のパスへの778件のリクエストがSQLインジェクションのペイロードを含んでいました。44 ユーザーエージェントのクエリも完全で、異なる文字列は73個です。普通のChromeの文字列とGooglebotの名乗りのほかに、スキャナーは149件のリクエストで54種類のSQLペイロードを、78件で3種類の引用符を壊す文字列を送り、12個のマーカートークンをそれぞれ1回ずつ送っていました。41

いくつかの文字列は、スキャナーが何を尋ねていたかを示すので、見ておく価値があります。(select 198766*667891 from DUAL)は、テーブルのないselectの中の算術式です。PortSwiggerのチートシートが示すとおり、FROM dualはそのためのOracleの構文です。33 if(now()=sysdate(),sleep(15),0)は、同じチートシートにある条件付きの時間遅延と同じIF(condition, SLEEP(n), x)の形をしています。33 @@qkLpPは、チートシートがMicrosoftとMySQL向けにSELECT @@versionで使う@@の接頭辞に5文字が続いたもので、スキャナーはこれに似たものを1ダース送っていました。3341 OWASPのテストガイドは、これらの文字列が使う2つの技法を名指ししています。「forces the database to generate an error」(データベースにエラーを生成させる)エラーベースのインジェクションと、「use database commands (e.g. sleep) to delay answers in conditional queries.」(データベースのコマンド(例:sleep)を使って条件付きクエリの応答を遅らせる)時間遅延です。45 私が取得したページのどれも、これらの文字列そのものを記載してはいないので、特定のツールに帰属させず、記述するだけにとどめます。

2026年11月2日、太平洋時間10:56から11:17まで、1つのスキャナーのアドレスがblakecrosley.comに送ったすべてのリクエスト(合計10,907件)を、1分ごとに1本の棒で示し、誰が応答したかで色分けした棒グラフ。ブロック前の棒は大半が配信されたページとアプリの拒否で、ピークは1分あたり637件。このアドレスのお問い合わせフォームへの投稿のうち62件がアプリに届き、そのうち31件に200が返った。200は送信か、ハンドラーが同じく200を返す失敗なので、メールは最大31通。内訳はブロックの分より前の数分間に26件、ブロックの分そのものに5件で、それ以降の分には0件。破線はモニターが置いた11:04:01 PST(19:04:01 UTC)のブロックを示し、スキャンの最初の1分から8分後。細い点線は、スキャンの最初の瞬間だけを見て何も見つけなかった10:57:00 PSTのモニターの実行と、フラッドの検出結果を出した11:02:46 PSTの実行を示す。それ以降の1分単位のバケットのリクエスト(合計7,445件)はすべてエッジでブロックされ、ピークは1分あたり1,107件。

11月2日:1つのアドレスからの10,907件のリクエスト、1分ごと。私のモニターの2回の実行と、モニターが11:04:01 PSTに置いたブロックを示しています。1419

4. 5つの層と、それぞれが見分けられたもの

これらのボットと私の受信箱のあいだには、5種類の制御がありました。それぞれについて、私は同じ問いを立てました。ボットと人間を見分けられたか、です。エッジのルールにある1つのマネージドチャレンジは訪問者にその問いを投げられますが、対象はユーザーエージェント文字列が一覧にあるリクエストだけで、スキャナーの文字列はそこにありませんでした。脅威スコアをキーにした2つ目のチャレンジは決して発動しえなかったので、すべての送信にその問いを投げるものは何もありませんでした。制御のうち2つは数を数え、1つは名前と一覧を照合し、1つはサイトが発行したトークンをクライアントが持っていることを証明し、1つは隠しフィールドを埋めるボットだけを捕らえます。以下、それぞれが何を確認し、何を止め、残りをなぜ通したのか、その正確な理由を見ていきます。

エッジのカスタムルール

何を確認するか。 このサイトでは、最初のカスタムルールが、アカウントレベルの一覧にあるアドレスすべてと、ASNで指定した7つのホスティングネットワークをブロックします。別のルールは、.php、/wp-、/.envのようなエクスプロイトのパスの部分文字列をブロックします。さらに2つが、お問い合わせフォームと管理用のパスにマネージドチャレンジを返します。1つは空のユーザーエージェント、またはcurl、wget、python-requests、httpx、scrapy、HeadlessChrome、PhantomJS、Seleniumを含むユーザーエージェントに対して、もう1つはCloudflareの高い脅威スコアに対してです。スタジオのサイトにも同じ種類のルールがあり、2つのチャレンジはサイト全体に適用され、APIのパスに対するブロックはお問い合わせフォームを除外しています。9 脅威スコアのチャレンジは、両サイトに1つずつ、死んだルールでした。Cloudflareのフィールドリファレンスによれば、このスコアはかつて0から100の範囲でしたが、「Now, the threat score is always 0 (zero).」(現在、脅威スコアは常に0(ゼロ)である)とあります。高いスコアを求めるルールは何にも一致しません。私はそれを読んだ11月3日に両方を無効にしました。ユーザーエージェントのチャレンジは残しています。1146 CloudflareのFreeプランでは5つのカスタムルールが使え、それらは「work as customized web application firewall (WAF) rules that you can use to perform actions like Block or Managed Challenge on incoming requests.」(受信リクエストに対してBlockやManaged Challengeのようなアクションを実行するのに使える、カスタマイズされたWebアプリケーションファイアウォール(WAF)ルールとして機能する)ものです。47

何を止めたか。 エクスプロイトのパスは、両日とも最初の1分から止めました。104 10月30日には、フォーム投稿を1件も止めていません。110 11月2日には、私のモニターのブロック後の438件のフォーム投稿を止めました。2 スタジオのサイトでは、フォーム投稿を1件も止めていません。8日間で、ファイアウォールはフォームへのPOSTについて1件のイベントも記録しませんでした。7

なぜ残りを通したか。 どのルールも、リクエストが何者であるかを見ていないからです。一覧のルールがアドレスに一致するのは、何かがそれを一覧に載せた後だけです。10月30日は人間が、11月2日は私のモニターが載せました。91019 ユーザーエージェントのチャレンジは文字列に一致するもので、2つのスキャナーはどちらもブラウザーの文字列を送っていました。初日は少なくとも69,322回、2日目は9,725回です。3241 ユーザーエージェントのチャレンジは、自分の正体を名乗るツールには確かに効きます。10月30日の私自身のcurlテストは、24件のマネージドチャレンジを受けました。39 脅威スコアのチャレンジは誰に対しても発動しえず、ファイアウォールはどちらのアドレスについても、フォームに対するチャレンジを1件も記録していません。11104 Torの出口ノードや、名指しした7つのASN以外のホスティングネットワークは、どの一覧にも載っていません。

エッジのルールは、既知のアドレス、既知のネットワーク、正直なツールをそれ以外から見分けられます。そのチャレンジが人間がいるかを問うのは、その式が選んだリクエストに対してだけです。スキャナーが送るブラウザーの文字列を、ブラウザーから見分けることはできません。

エッジのレート制限

何を確認するか。 ルールの文面は短いものです。9

(http.request.method eq "POST" and http.request.uri.path in {"/api/contact" "/paint/share"})

一致するリクエストをCloudflareのデータセンターとクライアントのアドレスごとに数え、10秒あたり3件まで許し、10秒間ブロックします。均等にならせば、その上限はデータセンターごと、アドレスごとに1分あたり18件に名目上相当します。ルール自体は10秒の期間で数えています。916 Freeプランでは、期間、タイムアウト、カウントのキーが固定されており、ゾーンにはこの種のルールが1つだけ与えられます。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

何を止めたか。 どちらの日も、スキャナーのリクエストは1件も止めていません。インシデントの時間帯にこのルールが出したイベントは、私自身のテストの3件だけです。10439 スタジオのサイトにも独自のサイト全体のレート制限ルールがあり、Cloudflareが検証済みボットとして挙げるクライアントからのものを除くすべてのリクエストを同じ方法で数えています。このルールは、ドリップ(少量ずつ続く投稿)のフォーム投稿を1件も拒否しませんでした。8日間で、ファイアウォールはフォームへの投稿について1件のイベントも記録していません。このルールはアドレスがサイトに送るそうしたリクエストすべてを数え、私のデータはフォーム投稿しか数えていないので、カウンターにどれだけ余裕が残っていたかは測れません。97

なぜ残りを通したか。 10月30日には、ルールは18:49:53 UTCまで存在しませんでした。手作業のブロックの最初のイベントと同じ秒で、アプリに最後のフォーム投稿が届いてから約12分後です。91 そのスキャナーの1,065件の投稿はすべて1つのデータセンター、パリを経由していたので、ルールがあれば、それらに対して1つのカウンターを保持していたはずです。5 11月2日にはルールが存在しており、スキャナーのフォーム投稿は18:56から19:05:00 UTCまでに2つのデータセンターを経由して届きました。アムステルダムが44件、ロンドンが41件です。最も多い1分間である19:02には、ロンドンが18件、アムステルダムが14件を運びました。5 1分間に18件はルールの上限に名目上相当しますが、私のデータは到着時刻ではなく1分ごとの合計なので、1つのデータセンターのいずれかの10秒間に3件を超えたかどうかは示せません。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 アプリに届いた最初の投稿から最後の投稿までの7分間で、スキャナーは1分あたり平均8.86件でした。2 当日の私のメモはそれを「about nine a minute」(1分あたり約9件)と呼んで、そこで止まっていました。データセンター間の分割こそ、メモが見落とした部分です。23

ルールがそもそも機能することを証明したかったので、11月3日の朝、自分のマシンからプローブしました。06:36:49 PSTに、CSRFトークンなしで10件のPOSTを0.4秒間隔で送りました。4件は必須項目の欠落でFastAPIが拒否して422が返り、続く6件は429が返りました。06:38:03 PSTには、すべての項目をそろえて8件を送りました。3件はオリジンのCSRF拒否である403が返り、続く5件はCloudflareから「error code: 1015」という本文とともに429が返りました。その数分間に、ファイアウォールはフォームについて11件のイベントを記録し、そのすべてがレート制限ルールによるブロックでした。18 ルールは発動し、Cloudflareはそれを記録します。11月2日についての私の読みは仮説です。2つのカウンターに分かれ、それぞれで名目上の上限近くで走っていたため、スキャナーはどちらも作動させなかった、というものです。1分ごとの合計に合う最も有力な説明ではあっても、実証ではありません。合計からは、各分の中で投稿がどう間隔を空けていたかはわからず、Cloudflareも、カウンターの更新中に超過したリクエストが通りうることを認めています。548

レート制限は、1つのデータセンターの1つのアドレスについて、速いか遅いかを見分けられます。誰が送っているかは見分けられません。

CSRFトークン

何を確認するか。 このサイトのフォームには、ページに埋め込まれた隠しトークンがあり、3つの部分から成ります。タイムスタンプ、ランダムな値、そして最初の2つに対するHMAC署名です。サーバーは、署名が一致し、タイムスタンプが1時間以内であればそれを受け入れます。15 スタジオのサイトのフォームには、CSRFトークンがありません。25

何を止めたか。 10月30日に4件の投稿(すべて18:03の分)、11月2日には0件です。12

なぜ残りを通したか。 トークンがステートレスだからです。セッションにもCookieにもアドレスにも結び付いていないので、ページを取得したクライアントは誰でも有効なトークンを手に入れられますし、別のクライアントからトークンを渡されたクライアントも、有効期間中はそれを提示できます。2つのスキャナーはどちらも実際にサイトのページを取得していました。リクエストのログがそれを示しています。チェックが示すことはもっと狭く、両日のすべての送信が、私のサイトが発行した有効で期限内のトークンを持っていた、ということです。ハンドラーは送信の前にトークンを確認するからです。1513142416 CSRFトークンは、別のサイトが訪問者のブラウザーに私のフォームを送信させるのを止めるためにあります。自分でフォームを送信したいクライアントを止めることは、もともと意図されておらず、実際に止めませんでした。

同じチェックには2つ目の欠陥があり、それはボットではなく人間に降りかかるものでした。トークンが失敗すると、ハンドラーはHTMXのフォームが一度も表示しないJSONの403を返していたので、トークンの期限が切れた人は送信ボタンを押しても何も見えなかったはずです。12 これは修正しましたが、最初の修正もうまく動きませんでした。その経緯は第6節で述べます。

有効なCSRFトークンが証明するのは、私のサイトが発行した期限内のトークンをクライアントが持っていたことです。誰がフォームに記入したのかも、そのクライアントがページを読み込んだことさえも、証明できません。

アプリのレートリミッター

何を確認するか。 このサイトのハンドラーはデコレーターの後ろにあり、そのデコレーターはクライアントのアドレスごと、時計の1分ごとのフォーム投稿をCloudflare KVで数え(メモリー内のフォールバック付き)、5件を超えると429で拒否します。17 スタジオのサイトには、匿名のクライアント向けにプロセスごとのメモリー内リミッターがあり、アドレスとクライアントの分類ごとに数えます。この分類は、リミッターがユーザーエージェントとアドレスから割り当てるクラスで、ヘルスチェックと静的ファイルを除くすべてのパスが対象です。25

何を止めたか。 10月30日に830件、11月2日に31件の投稿です。スタジオのサイトでは0件です。127

なぜ残りを通したか。 設計上、誰に対しても1分あたり5件を通すからです。しかも時間枠が時計の1分なので、分の境界をまたぐバーストには2回分の許容量が与えられます。17 そこまでは知っていました。件数をコードと照らし合わせて読むまで知らなかったのは、これが漏れるということで、しかもコードには漏れる経路が少なくとも3つあります。リミッターはKVからHTTPでカウンターを読み、上限と比較し、それからカウントに1を足した値を別のリクエストで書き込みます。そのため、どちらかが書き込む前に両方が読んだ2つの投稿は、両方とも通ります。インクリメントは書き込む前にカウンターをもう一度読み、その2回目の読み取りに失敗すると、書き込みがカウントを1に戻すので、その分はやり直しになります。そしてKVの読み取りが何も返さないか書き込みに失敗するたびに、判定は1つのプロセスのメモリーに保持したリストにフォールバックし、そのリストはほかのプロセスとは共有されません。17

件数が漏れを示しています。10月30日には、8つの分で5件を超える送信があり、18:06の分には18件ありました。1 1分5件の正確なリミッターなら、スキャナーが投稿した分を通して最大54件の送信しか許さなかったはずですが、101件が出ていき、上限を47件超えました。11月2日には19:02と19:03の分がそれぞれ9件と10件を通しました。正確なリミッターなら、それが見た62件の投稿のうち最大22件しか許さなかったはずですが、31件が出ていき、上限を9件超えました。3 どの経路が漏れたのかは、件数からはわかりません。私の読みでは、どちらかが書き込む前に複数の投稿が同じカウントを読んだか、負荷の下でKVの呼び出しが失敗してカウントがリセットされたり判定がプロセスごとのリストに渡ったりしたか、あるいはそれらの組み合わせです。手元のログはアプリが返したステータスを記録しているだけで、どの経路が判定したかは記録していないので、経路を区別できません。173 対処法はどの経路でも同じで、1行の変更では済みません。アトミックなインクリメントを持つカウンターストアです。KVの読み取りの後に別の書き込みを行う方式では、それは得られません。この漏れは私のバグです。同時にそれは、この記事の主張を、狙ってもできなかったほど鋭くしてくれます。5件と言いながら18件を通す制御が、誰が私に書き込めるかを決めるものになるはずは、最初からなかったのです。

スタジオのサイトでは、アプリのリミッターは観測されたフォーム投稿を1件も拒否せず、最も多く送ったアドレスでも1時間に13件でした。そのカウンターは、同じクライアントの分類のもとでアドレスがほかのすべてのパスに送るリクエストも取り込み、私のデータはフォーム投稿しか対象にしていないので、件数からカウンターにどれだけ余裕があったかは測れません。725

レートリミッターは、リクエストが順番に来るなら5件と6件を見分けられます。どんな件数でも、人間とスクリプトは見分けられません。

ハニーポット

何を確認するか。 スタジオのサイトのフォームには、人間には決して見えない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

何を止めたか。 8日間の52件中42件、80.8%の投稿です。到着した時間帯で見るとTorの出口ノードからの投稿すべてを含み、時間帯とメッセージのパターンで見ると、SQLスパイダーを自称するものの1回のバーストからの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 ほかより愚かでないボットもいて、リード獲得を売り込むものは中でも最も愚かでないものです。

ハニーポットが証明するのは、人間には見えないフィールドをクライアントが埋めたということだけです。空のハニーポットは何も証明しません。

3つのグループに分かれた横棒グラフ。グループはストリームごとに1つで、各棒はエッジに届いた送信に占める割合。blakecrosley.comへの10月30日のスキャナー:1,065件がエッジに届き、1,065件すべてがアプリに届き、101件に200が返った。200は送信か、ハンドラーが同じく200を返す失敗なので、受信箱に届いたのは最大101件。11月2日のスキャナー:500件がエッジに届き、62件がアプリに届き(ブロックの分より後は0件)、31件に200が返った(ブロックの分より前の数分間に26件、ブロックの分そのものに5件)ので、受信箱に届いたのは最大31件。941apps.comの8日間:52件がエッジに届き、52件すべてがアプリに届き、10件が保存されたので、メールで送られたのは最大10件。残りの42件には、ハニーポットが成功したかのように応答し、何も保存しなかった。

それぞれのストリームがどこまで進んだか:このサイトへの2つのスキャナーの投稿と、スタジオのサイトへの8日間のドリップ。1278

5つに共通していたこと

5つの結びの文を並べて読むと、隙間ははっきりします。レート制限はアドレスごとの量を抑えます。CSRFトークンは、サイトが発行したトークンをクライアントが持っていたことを証明します。ハニーポットは、クライアントが隠しフィールドを埋めたことを証明します。エッジのルールは名前と一覧を知っています。そして、向こう側に人間がいるかを問えるもの、つまりユーザーエージェントのチャレンジは、ユーザーエージェント文字列が一覧にあるリクエストにしか問いません。その隣の脅威スコアのチャレンジは決して発動しえませんでした。どちらのスキャナーも一致しなかったので、すべての送信に問いを投げるものは何もなく、スキャナーはブラウザーにできないことを何もしていませんでした。ページを取得し、そのトークンを保持し、制限が一部を通す速さで投稿しただけです。OWASPのクレデンシャルスタッフィングに関するガイダンスは、別のフォームについて同じ点を指摘し、「not rely on a single predictable volume limit」(単一の予測可能な量の制限に頼らない)防御を勧めています。50

5. スタジオのサイトへのドリップ

スタジオのサイトのフォームには、フラッドは一度も来ませんでした。来たのはドリップ、つまり少量ずつ続く投稿で、これはまったく別の生き物です。ここでの時間枠は、8日間のクエリでフォーム投稿のあった最初の時間帯である10月26日17:00 UTCから、私がデータを保存した11月3日13:59:50 UTCまでです。最初と最後の日は一部だけです。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日の急増は、イギリスの1つのアドレスがMozilla/5.0 (compatible; SQLSpider/1.5)というユーザーエージェントで、1時間のうちに13件を投稿したものです。時間帯とメッセージのパターンで見ると、テーブルはそのどれも保存していません。その時間帯に保存された1行はリード獲得の売り込みで、同じ時間帯に投稿していたアメリカのアドレスのパターンです。7820

誰が送っていたか

Cloudflareは、TorネットワークからのリクエストにT1という特別な国コードを付けます。ヘッダーのリファレンスには「T1 - Used for clients using the Tor network」(T1:Torネットワークを使うクライアントに使用)とあり、IP Accessルールのページには「T1: Tor exit nodes (country name: Tor)」(T1:Torの出口ノード(国名:Tor))とあります。5152 このコードで見ると、52件の投稿のうち23件(44.2%)がTorから来ており、35個の異なるアドレスのうち23個(65.7%)がTorの出口ノードでした。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 2つの情報源は一致しています。ただしそのリストは11月3日の出口ノードなので、その日のコードを裏付けるだけで、各投稿が届いた日にどのアドレスが出口ノードだったかは証明しません。プロジェクトのCollecTorアーカイブは過去の出口リストを保持しており、イベント当日の所属を確定できますが、私はそれをクエリしていません。55

この時間枠では、ほとんどのアドレスが1回だけ投稿していました。35個のうち32個(91.4%)で、Torの出口ノードはすべてその中にあります。7 2回以上投稿した3つは、13件のSQLスパイダー、1時間に2件を送ったポーランドのアドレス、そして5件を送ったアメリカのアドレスです。最後のアドレスの5件の投稿は、この時間枠でテーブルが保存した5件のリード獲得の売り込みと同じ5つの時間帯に収まっており、しかも毎回ユーザーエージェントを変えていました。Linux上のChrome 135、続いてFirefox 128、137、138、そしてまた137です。720

ユーザーエージェントも同じ線で分かれました。Torの投稿のうち18件はAndroid端末を、5件はデスクトップのChromeを名乗り、23件中21件はChrome 81以前を名乗っていました。Tor以外の投稿は、それよりはるかに新しいデスクトップブラウザーを名乗っていました。125から135のデスクトップChromeが10件、128から138のFirefoxが6件、そしてスパイダー自身の名前での13件です。7

誰が保存されたか

2つのグループはハニーポットで出会いました。この時間枠で保存された行のうち、Torの投稿しかない時間帯にあるものは1つもなく、10行すべてがTor以外の投稿のある時間帯にあります。Torの投稿23件のうち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は1つのことを対象外にもしています。「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」(壊れたフォームからメール、フォームからSMSへの機能を大量に悪用して意図しない受信者にメッセージを送ることは、この脅威イベントに含まれない)。56 私のフォームは私にしかメールを送らないので、含まれる側、つまりサイトの所有者に宛てたスパムであって、除外される側ではありません。

ドリップは、設計上レート制限も打ち負かします。OWASPのクレデンシャルスタッフィングに関するガイダンスは、リクエストを多数のアドレスに分散させる効果を名指ししています。「This may defeat both IP block-lists and rate limiting, as per IP request volume may remain relatively low」(IPごとのリクエスト量が比較的低いままでありうるため、これはIPブロックリストとレート制限の両方を打ち負かしうる)。50 アドレスごとに1件の投稿は、量としてこれ以上ないほど低いものです。

保存されたメッセージの中身

メッセージテーブルは6月6日までさかのぼり、私が読んだ時点で108行ありました。最新のものは11月3日07:24 UTCに届いています。8 私はそれらを固定のルールでパターン別に分けました。最初に一致したものを採用します。57

パターン テーブル全体 10月
「Hi, I wanted to know your price」(こんにちは、価格を知りたいのですが)、1行ごとに1つの言語、送り手の名前は1つ 49 10
業者の売り込み:Webのリード、Instagramの成長、解説動画、SEO、B2Bデータ、ディレクトリー、デザイン 22 4
賞品の誘い文句:ジャックポット、プロモーションコード、PlayStation、ランボルギーニ、ロールス・ロイスのリンク、送り手の名前は1つ 16 3
telegra.ph上の「$50,000,000 per month」(月5,000万ドル)のリンク 9 9
リード獲得の売り込み:「If I were a real customer, how soon would I get a call back?」(私が本物の顧客だったら、どれくらいで折り返しの電話をもらえますか?) 6 5
その他:意味不明な文字列2件、メーリングリストへの依頼、ほかの場所に書くよう求めるメモ 4 1
9月21日のメール通知機能の私自身のテスト 1 0
私の読みでは本物の問い合わせ 1 0

いくつかは、もう少し詳しく見る価値があります。$50,000,000の9行は、1つの送り手の名前と9つの異なる送り手のアドレスで届き、どれも同じ見出しを少しずつ変えたものでした。8 価格の質問は、テーブル全体で25の言語に現れます。アフリカーンス語、アルメニア語、バスク語、ベラルーシ語、ベンガル語、ブルガリア語、カタルーニャ語、クロアチア語、デンマーク語、英語、ジョージア語、ギリシャ語、ハンガリー語、アイスランド語、インドネシア語、アイルランド語、イタリア語、ラテン語、ラトビア語、リトアニア語、ルクセンブルク語、スペイン語、ベトナム語、ウェールズ語、ズールー語です。10月だけでも9つの言語で届きました。アルメニア語、バスク語、デンマーク語、インドネシア語、ラテン語、ラトビア語、リトアニア語、スペイン語、ベトナム語です。57 言語の判定のうち2つは私によるもので、微妙なところです。クロアチア語と読んだ文はボスニア語かもしれず、インドネシア語と読んだ文はマレー語かもしれません。57 誰かが、1言語ずつ、フレーズ集をお問い合わせフォームに通しているのです。

頻度は、ドリップのもう1つの特徴です。10月は32行を保存し、1日あたり1.03行、31日のうち21日に保存があり、1日に3行を超えた日はありませんでした。8 1日1件では、アドレスごとのレート制限はどれも作動しません。

どれが受信箱に届いたか

フォームがスタジオの受信箱にメールを送るようになったのは、9月21日17:46:47 UTCからです。それ以来、43行を保存し、つまりメールで送っています。これは上限です。通知の呼び出しはベストエフォートで、デプロイ時刻も確認していないからです。その日付より前に保存された65行は、一度もメールで送られていません。258 証拠の時間枠にある行は、すべてスパムです。57

私自身のテストを除くと、テーブル全体でスパムでない行は1つだけです。9月14日、スタジオの製品の1つを使う顧客が、特定のエラーについて人間の言葉で書き送ってきましたが、そのメッセージは誰にも読まれないままテーブルに置かれていました。57 その1週間後にフォームを受信箱へのメールにつないだとき、コミットメッセージはその理由を平易な言葉で書いています。「a paying customer’s message sat unread in D1 for a week」(有料顧客のメッセージがD1の中で1週間未読のままだった)。25 このメッセージこそ通知機能が存在する理由であり、通知機能を切ってスパムを解決できない理由でもあります。送り手はここでは匿名にしておきます。要点は、本物の人間がフレーズ集と同じテーブルにいる、ということです。

6. ボットチェックとは何か、そして何を出荷したか

Cloudflare自身の言葉で見るTurnstile

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経由にせずにどんなWebサイトにも埋め込め、訪問者に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は、向こう側にいるのがどんな種類のクライアントかを問います。私のユーザーエージェントのチャレンジもそれを問えましたが、対象はユーザーエージェント文字列が一覧にあるリクエストだけで、脅威スコアのチャレンジは決して発動しえませんでした。ここで重要な違いは、選択的に発動するチャレンジと、すべての送信に求められる検証との違いです。

ウィジェットには3つのモードがあります。私が使っているのはマネージドモードで、Cloudflareはこれを「Automatically chooses between non-interactive or checkbox challenge based on visitor risk level. No images or text to decipher.」(訪問者のリスクレベルに基づいて、操作不要のチャレンジかチェックボックスのチャレンジかを自動的に選ぶ。判読すべき画像や文字はない)と説明しています。58 平たく言えば、訪問者には短いチェックが見えるだけで、シグナルがそれ以上を求めない限り何もなく、求めた場合にはチェックを入れるボックスが表示されます。インビジブルモードはそれさえも隠しますが、Cloudflareはそれに条件を付けています。有効にしたサイトは、自身のプライバシーポリシーでCloudflareのTurnstile Privacy Addendumに言及しなければなりません。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.」(Turnstileの実装を完成させるには、Siteverify APIを呼び出さなければならない。クライアント側のウィジェットだけではフォームを保護しない)。さらに、もっと率直に「Tokens can be forged. An attacker can submit any string to your form endpoint without completing a challenge.」(トークンは偽造できる。攻撃者はチャレンジを完了せずに、フォームのエンドポイントに任意の文字列を送信できる)。60 サーバーはシークレットキーとともにトークンをhttps://challenges.cloudflare.com/turnstile/v0/siteverifyにPOSTし、訪問者のIPアドレスをremoteipとして含めることもできます。60

トークンの2つの性質が、その周りのコードの形を決めます。「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つ手に入れたスキャナーでも、それでフォームに1,065回投稿することはできません。Cloudflareはまた、トークンの長さを2,048文字までとし、サーバーに「Validate the action and hostname when specified.」(指定されている場合はactionとhostnameを検証せよ)と指示しています。60 同じページから1つ注意があります。期限切れのトークンについて、ある箇所ではtimeout-or-duplicateを挙げ、エラーの表ではinvalid-input-responseを「Token is invalid, malformed, or expired」(トークンが無効、不正な形式、または期限切れ)と説明しています。したがってサーバーは、1つのコードにロジックを結び付けるのではなく、どんな失敗も拒否として扱うべきです。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と関連するオリジンなど、さまざまなクライアント側のシグナル(「Signals」)を処理する)。目的もこう述べています。「The purpose of collecting these Signals is not to identify, profile or target any individuals but solely to detect and block bots.」(これらのSignalsを収集する目的は、個人を特定、プロファイリング、標的化することではなく、もっぱらボットを検出しブロックすることである)。そしてCloudflareがそれを製品の改善にも使うとしています。「Cloudflare is a data controller of Signals that we process to improve Turnstile’s bot detection capabilities.」(Cloudflareは、Turnstileのボット検出能力を改善するために処理するSignalsのデータ管理者である)。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が見るのは私のエッジがすでにすべてのリクエストについて記録しているのと同じ種類の接続データで、人が私のフォームに打ち込む内容は決して見ません。

必要なCSP

TurnstileはCloudflareのチャレンジ用オリジンから読み込まれるので、Content Security Policyでそれを許可する必要があります。Cloudflareは2つのディレクティブを挙げています。「script-src: https://challenges.cloudflare.com」と「frame-src: https://challenges.cloudflare.com」です。62 私の2つのゾーンは、どちらも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ページを返すことで、リクエストの流れを中断する。この仕組みは、AJAXやXHR(fetch)リクエストのように、ブラウザーがHTML以外のレスポンスを期待する場合には失敗する)。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.」(シングルページアプリケーション(SPA)やAPI連携を壊さずにAPI呼び出しを確実に保護するため、CloudflareはTurnstile Pre-clearanceの使用を推奨する)。63 プリクリアランスでは、解かれたウィジェットがcf_clearance Cookieも発行し、この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.」(顧客が設定したセキュリティクリアランスのレベルに基づいて、APIリクエストを含むゾーン上の以降のすべてのリクエストで、訪問者がWAFのチャレンジを回避できるようにする)ものです。Cookieはトークンの代わりにはなりません。Cloudflareは、それが「is issued to the visitor in addition to the default Turnstile token」(デフォルトのTurnstileトークンに加えて訪問者に発行される)と述べ、同じページでサーバーの確認を引き続き求めています。「You must call Siteverify to complete your Turnstile configuration.」(Turnstileの構成を完成させるにはSiteverifyを呼び出さなければならない)。65 プリクリアランスが加えるのは、XHRの投稿が生き残れるエッジのチャレンジで、対象はチャレンジのレベル以上の有効なクリアランスをCookieに持つ訪問者です。私はデフォルトを使いました。フォームの中のウィジェットと、私自身のサーバーが確認するトークンです。

11月3日に出荷したもの

両方のサイトで、CloudflareのAPIを通じてマネージドモードのウィジェットを作り、それぞれのサイト自身のドメインに範囲を限定しました。各ウィジェットのシークレットは、同じプロセスの中でAPIの応答から私のMacのキーチェーンとホスティングサービスの変数へと渡し、コマンドラインにもファイルにも一度も載せていません。12

このサイト。 新しい検証器が、訪問者のアドレスとともにトークンをsiteverifyにPOSTします。トークンがない場合、2,048文字より長い場合、拒否された場合、通信に失敗した場合は、すべてfalseを返すので、シークレットが設定されている限り、フォームはフェイルクローズになります。ローカル開発やテストスイートのようにシークレットが設定されていない場合、フォームは以前と同じように動きます。12 Cloudflareが受け入れたトークンは、このサイト自身のホスト名のいずれかで提供されたものでなければならず、Cloudflareがウィジェットのactionを返す場合は、それがフォーム自身のものでなければなりません。サーバー側のガイドが勧めるとおりです。ホスト名のない応答は拒否です。したがって、別のサイトや別のウィジェットで発行された有効なトークンは通りません。このチェックは同じ朝の2つ目の変更として入り、07:20 PSTからビルドしました。07:25 PSTにキャッシュをパージした後、ライブのページから取った有効なCSRFトークンを持ち、Turnstileのトークンを持たない投稿は、再び403で戻ってきました。これはデプロイ後もフォームがフェイルクローズのままであることを証明します。トークンのない投稿ではホスト名のチェックそのものを試せず、それはコードとテストに依拠しています。6660 3つ目の変更までは、両方のウィジェットと両方の検証器がlocalhostも受け入れていました。sitekeyは公開されているので、誰かのlocalhostで私のsitekeyを使って発行されたトークンは、siteverifyもホスト名のチェックも同じように通っていたはずです。Cloudflareのテストのページはそれを勧めていません。「Cloudflare recommends that sitekeys used in production do not allow local domains」(Cloudflareは、本番で使うsitekeyがローカルドメインを許可しないことを推奨する)とあり、その中にlocalhostを挙げています。08:12 PSTに両方のウィジェットからlocalhostを外し、両方の検証器はデフォルトでサイトとそのwwwホストだけを受け入れるようになりました。こうしてチェックはこのサイト自身のホスト名だけを受け入れ、ほかは何も受け入れません。ローカル開発では設定でホスト名を指定します。その変更の後、両方のサイトは08:15と08:18(太平洋時間)に同じライブチェックに再び合格しました。ページ上のウィジェットと、403で拒否されるトークンのない投稿です。ホスト名のルールそのものを試せるライブチェックはありません。トークンがないかゴミのトークンなら、ホスト名が読まれる前に失敗するからです。そのルールは、ウィジェットの設定、コード、テストに依拠しています。4667 ハンドラーのチェックの順序は現在、CSRF、次にTurnstile、次に送信で、以前と同じアドレスごとのリミッターの後ろにあります。どちらの拒否もページ自身のエラー断片を403で返しますが、HTMXはデフォルトでは4xxの応答をページに差し込まないので、フォームには403を通すハンドラーが必要です。そのハンドラーの最初のバージョンは動きませんでした。私はそれをフォームに付けましたが、HTMXは応答を受け取る要素でイベントを発火させ、このページではその要素はフォームの兄弟要素なので、イベントはハンドラーに届かず、拒否された人にはやはり何も見えませんでした。この記事の草稿の2回目のレビューが、同梱のHTMXとテンプレートを突き合わせて読み、それを見つけました。修正はその朝の4つ目の変更で、リスナーをページのbodyに置き、応答欄に範囲を限定したもので、それが描画されることをテストで確認しています。そのデプロイ後の08:21 PSTに、ウィジェットのトークンをゴミの文字列に置き換えて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 私の最初のバージョンは引数なしでリセットを呼んでおり、それはガイドに記載されていません。4回目のレビューのラウンドがそれを見つけ、現在は両方のサイトで呼び出しがコンテナを指定しています。11月3日のブラウザーでのチェックでは、各フォームにゴミのトークンを送信して拒否を確認し、その後、隠し入力に新しいトークンが入ったことを確かめました。スタジオのサイトでは8秒以内、このサイトでは25秒以内で、こちらではウィジェットが先にチャレンジを再実行していました。12 ウィジェットのdivとスクリプトは、サイトキーが設定されている場合にだけ描画されます。11個のテストが通ります。シークレットがなければ以前と同じように送信すること。シークレットがあれば、ハンドラーはどの送信よりも前にトークンのない場合を拒否し、拒否されたトークンも拒否し、受け入れられたトークンなら送信し、CSRFが引き続き先に実行されること。そして検証器がこのサイトのホスト名を受け入れ、別のホスト名、デフォルトでのlocalhost、別のaction、またはホスト名なしを拒否することです。126646

スタジオのサイト。 ハニーポットは前に残しています。隠しフィールドが埋まっていれば、引き続き成功メッセージで応答して何も保存しないので、最も安上がりなボットのためにCloudflareへの呼び出しが発生することはありません。次にTurnstileが来て、トークンがない場合、拒否された場合、検証できない場合は、エラー断片とともに403を返します。その後にバリデーション、メッセージテーブルへの挿入、通知が続きます。12 その検証器も同じ方法でホスト名(現在はサイト自身の2つのホスト名だけ)とactionを確認します。その変更は07:19 PSTにデプロイされ、07:22 PSTにはライブのページがウィジェットとサイトキーを持ち、トークンのない投稿とゴミのトークンの投稿は403で戻り、ハニーポットの投稿は引き続き200で応答しました。66 デプロイの前に、Cloudflareが文書化しているテスト用キーに対してハンドラーをローカルで実行しました。常に合格するシークレットでは、トークンのない投稿は403で戻り、ダミーのトークンはTurnstileを通ってバリデーションに達しました。常に失敗するシークレットでは、どちらも403で戻りました。ハニーポットは200で応答しました。1267 Cloudflareのテストのページは、ダミーのトークンが本番で通らない理由を述べています。「Production secret keys: Only accept real tokens, reject dummy tokens.」(本番のシークレットキー:本物のトークンだけを受け入れ、ダミーのトークンは拒否する)。67

ライブチェック。 その朝早く、それぞれのサイト自身のデプロイが成功し、キャッシュをパージした後、06:35 PSTに、それぞれのトップページには1つのウィジェットのdiv、1つのTurnstileのscriptタグ、そしてサイトキーがあり、各ゾーンのライブのポリシーはframe-srcにチャレンジ用オリジンを挙げていました。このサイトのフォームへのゴミのCSRFトークンを持つ投稿は403で戻りましたが、それはCSRFチェックが拒否したもので、Turnstileについては何も証明しません。Turnstileのチェックそのものは06:43 PSTに試しました。ライブのページから取った有効なCSRFトークンを持ち、Turnstileのトークンを持たない投稿は、「error: Verification failed. Reload the page and try again.」というメッセージとともに403で戻り、同じ投稿にゴミのTurnstileトークンを付けても403で戻りました。つまり、シークレットはサービスに設定されており、チェックはCSRFの後に実行されています。スタジオのサイトでは、トークンのない投稿とゴミのトークンの投稿がどちらも403で戻り、ハニーポットの投稿は200で戻って、何も保存されずメールも送られませんでした。70

ブラウザーでのチェック。 Playwrightで操作する本物のChromiumで、両方のページでウィジェットが描画され、Turnstileのスクリプトが読み込まれ、隠しのcf-turnstile-response入力は、両方のページで6秒以内に730文字のトークンを保持しました。70 これは見かけほど多くを証明しません。Cloudflareのテストのページは「Automated testing suites (like Selenium, Cypress, or Playwright) are detected as bots by Turnstile」(自動テストスイート(Selenium、Cypress、Playwrightなど)はTurnstileによってボットとして検出される)と警告しており、トークンが通るかどうかを決めるのはsiteverifyだけです。67 このチェックが証明するのは、ウィジェットが両方のポリシーの下で動き、自分のフィールドを埋めるということです。まだ行っていないのは、この記事の読者にとって最も重要な1つのテスト、つまり人間がフォームに記入して返信を受け取ることです。そのテストはメールを送るので、私が行うべきもので、まだ行っていません。

7. レート制限がまだ何のためにあるのか

Turnstileは、門としてのレート制限に取って代わりました。柵としてのレート制限に取って代わったわけではありません。ボットチェックは「誰が」に答えます。「何件か」には、まだ何かが答えなければなりません。空でない有効なサイズのトークンを持ってチェックに届く送信はすべて、今ではCloudflareのsiteverify APIへの呼び出し1回分のコストがかかり、通った投稿はすべて、引き続き送信1回分のコストがかかるからです。12 Cloudflare自身のサーバー側のガイドは、siteverifyの呼び出しそのものについての推奨事項の中にレート制限を挙げています。「Rate limit to protect against validation flooding.」(検証のフラッドから守るためにレート制限をかける)。60 私の2つの制限は、どちらも残します。データセンターごと、アドレスごとに10秒あたり3件の投稿というエッジのルールと、アドレスごとに1分あたり5件の投稿というアプリのリミッターです。917

どう数えるのかがわかった今、エッジのルールには正直な説明が必要です。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 2つのデータセンターを経由して私のゾーンに届くアドレスは、2回分の許容量を得ます。そして11月2日のスキャナーの投稿は、実際に2つを経由して届きました。それが、ルールが沈黙していた理由についての私の最も有力な説明で、1分ごとの合計と矛盾しないものの、合計によって証明されてはいません。5 これに、ルールは「are not designed to allow a precise number of requests to reach your origin server」(正確な数のリクエストをオリジンサーバーに到達させるようには設計されていない)というCloudflareの精度についての但し書きを加えると、このルールは証明された上限ではなく、名目上の予算として説明するのが最も適切です。データセンターごと、アドレスごとに10秒あたり3件のフォーム投稿、名目上1分あたり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 そのため、エッジのルールはブロックのままにし、チャレンジはウィジェットの中に置きます。

アプリのリミッターには、その漏れが求める修正が必要です。2つの投稿が同じカウントを読むことのないアトミックなインクリメントを持つカウンターストアと、カウントをプロセスごとに分割しない失敗時の経路です。現在のコード、つまりKVの読み取りの後に別の書き込みを行い、プロセスごとのフォールバックを持つ方式は、そのどちらも提供しないので、これは1行のパッチではなくストアの変更になります。173 受信箱に届きうるすべての送信にTurnstileが条件として加わったので、漏れが通した投稿も、Cloudflareが受け入れるトークンを持っていなければなりません。そのため漏れのコストは量とお金になり、誰が投稿できるかの判断はもはやカウントには依存しません。リミッターは予算です。1回のフラッドで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 私の2つのスキャナーは実際にサイトのページを取得していました。それが、有効なCSRFトークンを持つに至った最もありそうな経路です。ウィジェットを実行せずにフォームに投稿するクライアントには、送るべきTurnstileのトークンがなく、両方のフォームは今ではトークンのない投稿を拒否します。このサイトでは、有効なCSRFトークンを持ちTurnstileのトークンを持たない投稿が403で戻り、スタジオのサイトでもトークンのない投稿が同じく403で戻りました。70

チェックリスト

Cloudflareの背後にあるお問い合わせフォームについて、今の私ならこの順に行います。

  1. フォームにボットチェックを置き、すべてのトークンをサーバーで検証する。 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
  2. フェイルクローズにする。 トークンがない、大きすぎる、拒否された、検証できない場合は拒否です。「Tokens can be forged.」(トークンは偽造できる)からです。6012
  3. 送信のたびにウィジェットをリセットする。 トークンは使い捨てで有効期間は300秒なので、リセットなしの再試行は、使用済みのトークンでの再試行になります。6069
  4. チャレンジ用オリジンを両方のCSPディレクティブで許可する。 Cloudflareはhttps://challenges.cloudflare.comについてscript-srcとframe-srcを求めており、default-src 'self'だけのポリシーはiframeをブロックします。6212
  5. XHRやfetchで投稿するフォームにマネージドチャレンジをかけない。 Cloudflareによれば、チャレンジページは「fails when the browser expects a non-HTML response」(ブラウザーがHTML以外のレスポンスを期待する場合に失敗する)もので、HTMXの投稿はその種類のリクエストです。63
  6. 人間に見えるエラーを返す。 私のCSRFの拒否はHTMXのフォームが一度も表示しないJSONの403だったので、トークンの期限が切れた人は送信ボタンを押しても何も見えなかったはずです。また最初の修正はフォームでイベントを待ち受けていましたが、HTMXは差し込みのイベントを応答のターゲット、つまりフォームの兄弟要素で発火させます。拒否はブラウザーで試してください。1268
  7. ハニーポットをボットチェックの前に置き続ける。 私のスタジオのサイトでは、隠し入力1つとif1つのコストで、52件中42件の投稿を捕らえました。725
  8. レート制限は残し、門ではなくコストの上限として設定する。 Cloudflareはデータセンターごと、アドレスごとに数え、11月2日のスキャナーは2つを経由して届きました。それが一度も発動しなかった最もありそうな理由ですが、1分ごとの合計でそれを証明することはできません。65
  9. アプリのリミッターを、アトミックにインクリメントするストアで支える。 私のリミッターは、カウントを読んで別のリクエストで書き戻し、2回目の読み取りに失敗するとカウントを1に戻し、KVが失敗すると1つのプロセスのメモリーにフォールバックし、自身の上限を47件と9件超える送信を通しました。どの経路が漏れたのかは、ログからはわかりません。173
  10. スキャナーを止めるのにユーザーエージェントのルールを信頼しない。 2つのスキャナーはどちらも普通のChromeの文字列を送っており、10月30日は少なくとも69,322回、11月2日は9,725回でした。3241
  11. メールはフォームではなくメールサービスで数える。 私のハンドラーは失敗の経路でも200を返しますし、Resendのemail.sentイベントが意味するのは「the API request was successful」(APIリクエストが成功した)ということだけです。その後どうなったかは、取得したメールのlast_eventフィールドが示します。242873
  12. 1か月以内に分析をクエリする。 Cloudflareの適応型データセットは「at least 31 days of data on every plan」(すべてのプランで少なくとも31日分のデータ)を保持します。27
  13. 時間枠が閉じてから数え直す。 10月30日のフラッドの数分後に書いた私のメモは、Cloudflareが最後の数分の取り込みを終えていなかったため、フォーム投稿を少なく数えていました。16
  14. アドレスのブロックは後片付けとして扱う。 私のブロックは、10月30日には最初のフォーム投稿から約50分後、11月2日にはスキャン開始から8分後に入り、どちらの日もブロックの分より後に出た送信はありませんでした。10月30日はブロック前に101件、11月2日はブロック前に26件、ブロックの分の中に5件です。1192

何が変わりうるか

これらは予測ではなく条件です。それぞれ、何が起きなければならないか、そしてそのとき私が何をするかを示しています。

スキャナーがマネージドチャレンジを通り始めたら。 Turnstileのチェックはブラウザーの中で動き、自動化の痕跡を探します。それらを通るほど完全なブラウザーを大量に動かすツールにとって、ウィジェットは壁ではなく、もう1つのコストになるでしょう。兆候は、siteverifyが受け入れるトークンを持つフォームの送信が、User-Agentヘッダーにインジェクションの文字列も送るアドレスから来ることです。Batchelderは2007年に、ハニーポットについてこの正直な見通しを書いています。「If and when they do, we can switch back to CAPTCHAs.」(もしそうなったら、そのときはCAPTCHAに戻せばよい)。49 私の代替策も精神は同じで、より強いチャレンジです。それまでのあいだも、ハニーポットとレート制限は立ち続けています。

プリクリアランス付きのエッジのチェックを加えたら。 Cloudflareは、チャレンジページを描画できないリクエストにはTurnstileのプリクリアランスを推奨しており、cf_clearance Cookieは訪問者が「bypass WAF Challenges on all subsequent requests on the zone, including API requests」(APIリクエストを含むゾーン上の以降のすべてのリクエストでWAFのチャレンジを回避する)ことを可能にします。6563 それを採用すれば、エッジはチャレンジのレベル以上の有効なクリアランスをCookieに持つ訪問者についてHTMXを壊さずに、フォームのPOSTそのものにチャレンジをかけられます。CloudflareのCSPのページは、そのモードに1つの要件を加えています。「your connect-src directive must include ‘self’.」(connect-srcディレクティブに’self’を含めなければならない)。62 Cookieは「in addition to the default Turnstile token」(デフォルトのTurnstileトークンに加えて)発行され、siteverifyは引き続き必須なので、エッジのチャレンジは私のサーバーのチェックの前に置かれる2つ目のチェックであって、その代わりではありません。65 私はまだそれを行っておらず、この記事が説明しているのは稼働中のウィジェットとsiteverifyの設計です。

2つの時間枠について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 8日分についてそれをクエリすれば、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.」(人間のTorユーザーとボットを当社のOnionサービスで区別でき、操作を必要とするチャレンジは悪意あるボットのトラフィックにだけ提供される)とのことです。75 私はそれを有効にしておらず、有効にする前に、Turnstile導入後のスタジオのサイトのTorトラフィックを測りたいと考えています。

要点

お問い合わせフォームを運用しているなら:

  • CSRFトークンとレート制限を組み合わせても、スキャナーはあなたに書き込めます。私の2つのスキャナーはどちらも有効なトークンを持っており、私のリミッターは101件と31件の送信を通しました。12
  • ボットチェックのトークンはサーバーで検証し、フェイルクローズにし、リクエストのたびにウィジェットをリセットしてください。ウィジェットだけでは「does not protect your forms.」(フォームを保護しない)のです。60
  • リミッターは、1回に1リクエストずつではなく、負荷の下と失敗の下で試してください。読んでから書くカウンターにプロセスごとのフォールバックを組み合わせたものは、5件を許す設定の1分間に18件の送信を通しました。117
  • ハニーポットは残してください。スタックの中で最も安上がりなフィルターで、私のスタジオのサイトでは8日間、ハンドラーがアドレスを確認したり、行を保存したり、メールを送ったりする前に、これらの送信の80.8%を捕らえました。725

サイトのエッジを運用しているなら:

  • 自分のレート制限が何を数えているかを知ってください。Cloudflareでは、すべてのルールがデータセンターごとに数え、2つを経由して届くスキャナーは2つのカウンターを得ます。65
  • 自分のルールをプローブし、後でファイアウォールイベントを読んでください。私のプローブによる11件のファイアウォールイベントは、ルールがなぜ沈黙していたのかを1日考え続けるよりも多くのことを教えてくれました。18
  • XHRやfetchのPOSTにチャレンジページでチャレンジをかけないでください。Cloudflareはそれが失敗することを文書化しており、その場合にはプリクリアランスを推奨しています。63
  • ユーザーエージェントのルールが捕らえるのは正直なツールだけだと考えてください。ここでのスキャナーはブラウザーの文字列を送り、私のcurlのテストはチャレンジを受けました。3239

受信箱を読むのがあなたなら:

  • 受信箱が埋まったら、件名より先に送信元を確認してください。私のメールの流れのうち3つは1つの送信元アドレスを共有しており、それが最初の推測を誤らせた理由です。23
  • メールサービスのログが別のことを示すまでは、フォームの200をメールの件数ではなく上限として扱ってください。2428
  • 通知機能を切る前に、保存されたメッセージを読んでください。かつて本物の顧客のメッセージが1週間未読のままだったことがあり、そのメッセージこそ、スタジオのフォームがそもそも私にメールを送る理由です。25

よくある質問

Cloudflareを使っているのに、なぜお問い合わせフォームにスパムが届くのですか?

おそらく、フォームの前にあるどれも、すべての送信に対して人間が送ったのかを問うていないからです。Cloudflareのカスタムルールはアドレス、ネットワーク、パス、ユーザーエージェント文字列に一致し、レート制限はデータセンターごと、アドレスごとにリクエストを数えます。ブラウザーのユーザーエージェントを送り、先にページを読み込み、ほどほどの速さで投稿するスキャナーは、そのすべてを通り抜けます。私のサイトでは2つのスキャナーがまさにそうして101件と31件の送信を通し、私のスタジオのサイトのスパムは、私が測った8日間、大半がアドレスごとに1件の投稿で届きました。アドレスごとのカウントでは、これは捕らえられません。96127

レート制限はボットを止めますか?

レート制限は、1つのアドレスがどれだけ速く進めるかを抑えるもので、ボットと人間を見分けはしません。Cloudflare自身のドキュメントは、そのルールが「are not designed to allow a precise number of requests to reach your origin server」(正確な数のリクエストをオリジンサーバーに到達させるようには設計されていない)こと、そしてカウンターがデータセンター間で共有されないことを述べています。私のエッジのルールはどちらのスキャナーにも一度も発動せず、1分5件に設定したアプリのリミッターは1分間に18件を通しました。そのカウンターは読み取りの後に別の書き込みを行うもので、2回目の読み取りに失敗すると1にリセットされ、ストアが失敗するとプロセスごとのフォールバックが引き継ぎます。どの経路でも、余分な投稿が通りえます。48641173

ハニーポットのフィールドで十分ですか?

隠しフィールドを埋めるボットには十分で、私のスタジオのサイトでは、それがこれらの送信の80.8%でした。8日間で52件中42件の投稿を捕らえ、到着した時間帯で見ると、そこにはTorの出口ノードからの投稿すべてが含まれていました。隠しフィールドを空のままにする送り手には何もできず、通した10件(保存され、つまり最大10通のメール)はまさにそうしていました。720 2007年にこの技法を説明したNed Batchelderも、そう述べています。「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対策は、別のサイトが訪問者のブラウザーにあなたのフォームを送信させるのを止めるためのものです。私のトークンはそれより弱く、ステートレスで署名付き、有効期間は1時間で、セッションにもCookieにもアドレスにも結び付いていません。したがって証明するのは、私のサイトがその1時間以内に発行したトークンをクライアントが持っていることだけで、リクエストがどこから来たかではありません。どちらの種類も、ページを読み込んで自分でフォームを送信するクライアントは止めません。ページを読み込むことこそ、クライアントが有効なトークンを得る方法だからです。2つのスキャナーはどちらも有効なトークンを集めており、チェックが拒否したのは、2回のフラッドを通して彼らの投稿のうち4件でした。1512

ログでスキャナーと人間を見分けるにはどうすればよいですか?

フォームだけでなく、そのアドレスがしたことすべてを見てください。私の2つのスキャナーは何千ものパスをクロールし、パスとUser-AgentヘッダーにSQLインジェクションの文字列を入れ、大半のリクエストでChromeのユーザーエージェントを送り、一部ではCloudflareの検証済みボットのルールが受け入れなかったGooglebotの名乗りを送り、1分あたり数十から数百件のバーストでフォームに投稿していました。31324441 sleep(15)やwaitfor delay '0:0:15'のような時間ベースのペイロードは、文書化されたプローブのファミリーに属します。3377 人間は、実際に読んだページから1通のメッセージを送り、あなたのサーバーに眠るよう求めたりはしません。

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 つまりTurnstileが見るのは接続データで、人が打ち込む内容ではありません。

このサイトの関連記事。 Claude Code の auto モードはセキュリティ境界ではないは、別の制御について同じ主張をしています。各アクションを判定する分類器は壁ではない、という主張です。Agents.txtはアクセス制御ではありませんは、ボットに方針を宣言することと、それを強制することのあいだに線を引いています。アラート疲れ:セキュリティモニターから1日82通のメールは、11月2日のスキャナーを私のモニターの側から扱っています。最初の検出結果から8秒後、スキャン開始から8分後のブロック、そしてそれについてのメールは0通です。寝る前に実行することは、別の種類の運用ルーティンで、15,000ページのステータス、応答時間、キャッシュを毎晩確認するものです。AIエージェントを無人で実行すると実際に何が壊れるのかは、監督なしのエージェントのセッション、つまり私のインフラを動かしている種類の自動化の失敗モードを分類しています。

出典

著者のデータについての注記:私自身のシステムから得た件数はすべて、2つのゾーンについてのCloudflareのGraphQL Analytics API(リクエストとファイアウォールイベントのデータセット)、スタジオのサイトのメッセージテーブル、私のモニターのログ、そして2つのフォームのコードから来ており、すべて2026年11月3日に保存し、スクリプトで台帳に変換したものです。時刻は、PDTまたはPSTと明記したもの以外はUTCです。その証拠に含まれるアドレス、ルールや一覧の識別子、キー、ファイルシステムのパスは、この記事には一切載せていません。


  1. 著者のデータ。このサイトのゾーンについてのCloudflareのリクエスト分析。10月30日のスキャナーによるお問い合わせフォームへのPOSTを、1分ごと、エッジのステータス別に、2026年11月3日に閉じた時間枠に対してクエリしたもの:投稿1,065件、うち200が101件、429が830件、422が130件、403が4件で、すべてアプリに到達。最初の投稿と最初の200は18:00の分、ピークは18:03の分の208件、1分間で最も多い200は18件(18:06)、200が5件を超えた分は8つ、最後の200は18:36、アプリに届いた最後の投稿は18:37。台帳での導出値:ブロック前の送信101件、最初の投稿は私がフラッドを報告する約46分前、ブロックの49.88分前、アプリに届いた最後の投稿はブロックの11.88分前。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  2. 著者のデータ。このサイトのゾーンについてのCloudflareのリクエスト分析。11月2日のスキャナーによるお問い合わせフォームへのPOSTを、1分ごと、エッジのステータス別に、2026年11月3日にクエリしたもの:投稿500件、うち200が31件、429が31件、ブロックの分以降の403が438件。アプリに届いたのは62件。最初の投稿は18:58の分、アプリに届いた投稿のピークは19:02の分の32件、1分間で最も多い200は10件(19:03)、最後の200は19:04の分。アプリに届いた最初の投稿(18:58の分)から最後の投稿(19:04の分)までの7分間で平均1分あたり8.86件。送信31件のうち、ブロックの分より前の数分間に26件、ブロックの分そのもの(19:04、アドレスが19:04:01に一覧に載った分)に5件で、1分単位のバケットではその秒との前後を決められない。それ以降は0件。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  3. 台帳における著者の分析:各分について、5とアプリのリミッターが見た投稿数(200、429、アプリのCSRFによる403。リミッターが動く前にFastAPIが拒否する422は含めない)の小さいほうを取り、分を通して合計したもの。10月30日:リミッターが見た935件の投稿に対し、正確なリミッターなら最大54件の送信、実測は101件で47件超過。11月2日:62件に対し最大22件、実測31件で9件超過。これは反実仮想の上限であって測定値ではなく、測定された送信は上記のフォームの件数である。 ↩↩↩↩↩↩↩

  4. 著者のデータ。11月2日のアドレスについてのCloudflareのファイアウォールイベント、2026年11月3日にクエリ(サンプリングされたデータセットで、どのルールがいつ動いたかにだけ使い、量には決して使わない):最初のイベントは18:57:11のエクスプロイトパスのルールによるもの、ブロックリストのルールの最初のイベントは19:04:03。すべての行がブロックで、マネージドチャレンジは0。お問い合わせフォームについての行は310で、すべてブロック、最初は19:04:05。このアドレスについてレート制限ルールのイベントはなし。 ↩↩↩↩↩↩↩↩↩↩

  5. 著者のデータ、台帳の補遺。2つのスキャナーのフォームへのPOSTについて、Cloudflareのデータセンター、分、ステータスでグループ化したCloudflareのリクエスト分析、2026年11月3日にクエリ:11月2日の18:56から19:05:00(クエリの終端なので、19:05の分については最初の投稿だけを含む)まで、アムステルダム経由44件、ロンドン経由41件。19:02の分には、ロンドン経由18件(送信4、アプリが拒否14)、アムステルダム経由14件(送信5、拒否9)。1つのデータセンターの1分間で最大の件数は19:04の分のアムステルダムの19件で、そのうち15件はブロック後にエッジで拒否されたもの。10月30日は、1,065件の投稿すべてがパリ経由。 ↩↩↩↩↩↩↩↩

  6. Cloudflare、“How Cloudflare determines the request rate”、Cloudflare WAFのドキュメント、2026年11月3日14:08:57 UTCに取得。特性の組み合わせごとに別々のカウンター。同じ地理的位置にあるものを除き、カウンターはデータセンター間で共有されない。cf.colo.idは必須の特性。グローバルなカウンターはない。 ↩↩↩↩↩↩↩

  7. 著者のデータ。スタジオのサイトのゾーンについてのCloudflareのリクエスト分析。お問い合わせフォームへのPOSTを、時間帯、アドレス、国、ユーザーエージェント別に、2026年10月26日17:00 UTCから11月3日13:59:50 UTCまで:投稿52件、すべて200、35アドレスから。CloudflareのTorの国コードを持つ投稿23件とアドレス23個(44.2%と65.7%)。投稿がちょうど1件のアドレス32個(91.4%)で、Torのアドレスはすべてその中。繰り返し投稿した3アドレスは13件(イギリス、1時間、SQLSpiderのユーザーエージェント)、2件(ポーランド、1時間)、5件(アメリカ、毎回新しいユーザーエージェント)。Torのユーザーエージェントは18件がAndroid、5件がデスクトップのChromeで、21件がChrome 81以前を名乗る。Tor以外のユーザーエージェントはデスクトップのChrome 125から135が10件、Firefox 128から138が6件、その他13件。この時間枠で、ゾーンのファイアウォールはフォームへのPOSTについてイベントを1件も記録していない。ハニーポットにかかった数は投稿数から保存された行数を引いたもので、42件、つまり80.8%。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  8. 著者のデータ。スタジオのサイトのお問い合わせメッセージのテーブル、2026年11月3日14:04 UTCに読み取り:6月6日から11月3日07:24 UTCまでの108行、うち10行がクエリ後の時間枠内。日ごとの内訳は第5節の表のとおり。10月の32行は31日中21日にあり、1日あたり1.03行、1日に3行を超えた日はない。$50,000,000の9行は1つの送り手の名前と9つの送り手のアドレス。通知機能のコミット以降が43行、それ以前が65行。 ↩↩↩↩↩↩↩↩↩↩↩

  9. 著者のデータ。両サイトのゾーン設定、2026年11月3日にCloudflareのAPIでエクスポート:このサイトのレート制限ルール(バージョン1、最終更新2026-10-30 18:49:53 UTC。式は引用のとおり。特性はデータセンターとクライアントのアドレス。10秒の期間あたり3リクエスト。10秒の緩和タイムアウト。データセンターごと、アドレスごとに1分あたり18件に名目上相当)と、5つのカスタムルール(アカウントの一覧と7つのASNのブロック、エクスプロイトパスのブロック、検証済みボットのスキップ、そしてお問い合わせフォームと管理用パスに対する、ユーザーエージェント文字列によるものと脅威スコアによるものの2つのマネージドチャレンジ。どちらもエクスポート時点では有効で、脅威スコアのものはその日のうちに無効化)。スタジオのサイトの同じ種類のルール(チャレンジはサイト全体、APIパスのブロックはフォームを除外)と、検証済みボットではないクライアントからのリクエストを対象に、データセンターとアドレスごとに数えるサイト全体のレート制限ルール。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  10. 著者のデータ。10月30日のアドレスについてのCloudflareのファイアウォールイベント、2026年11月3日にクエリ(サンプリングされた3,932行で、クエリの上限未満。帰属とタイミングにだけ使用):最初のイベントは17:59:53のエクスプロイトパスのルールによるもので、このルールはスキャンの最初の1分から動いていた。18:49:53より前のサンプル111件はすべてそのルールによるもの。ブロックリストのルールの最初のイベントは18:49:53。このアドレスについて、レート制限ルールのイベントはなく、お問い合わせフォームについてのイベントもない。 ↩↩↩↩↩↩↩↩↩↩↩

  11. Cloudflare、“cf.threat_score”、Ruleset Engineのフィールドリファレンス、2026年11月3日16:12:44 UTCに取得。フィールドの説明:かつて0から100の範囲だったスコアで、「Now, the threat score is always 0 (zero).」(現在、脅威スコアは常に0(ゼロ)である)。 ↩↩↩

  12. 11月3日に出荷したものについての著者の記録、セッションのツール出力から作成:2つのウィジェット(マネージドモード、CloudflareのAPIで14:13 UTCごろ作成)、両ゾーンのCSPの変更、このサイトの検証器とハンドラーの順序(フェイルクローズ、CSRF、次にTurnstile、次に送信、403のエラー断片、HTMXのリクエストのたびにウィジェットをリセット)、10個の新しいテスト(10個合格。11個目はホスト名のチェックとともに加わった)、スタジオのサイトのハンドラーの順序(ハニーポット、次にTurnstile、次にバリデーション、保存、通知)、そしてCloudflareのテスト用キーに対するローカルでの確認。両サイトとも、検証器はトークンがない場合や2,048文字より長い場合、siteverifyを呼ばずに拒否する(コードから読み取り)。両サイトとも14:32から14:36 UTCのあいだにデプロイ。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  13. 著者のデータ。10月30日のアドレスについてのCloudflareのリクエスト分析、1分ごとと、私のアカウントのすべてのゾーンの横断で、2026年11月3日にクエリ:17:59の分から18:51の分まで(53バケット)でこのサイトへのリクエスト76,429件、両クエリで同じ合計。ブロックの分より前が69,484件で1分あたり平均1,389.7件、ピークは18:36の3,219件、ブロックの分より後が5,234件で配信は0。結果別では、配信49,513件(64.8%)、アプリが拒否18,990件(24.8%、うちサイトのエージェントゲートによるGET 418が10,019件)、オリジンの520が2,339件(3.1%)、エッジでブロック5,587件(7.3%)。スキャンの最初の1分からブロックまで50.88分。kiradex.appへのGETが387件ですべて200、ほかのゾーンへの接触はなし。 ↩↩↩↩↩↩↩↩↩↩

  14. 著者のデータ。11月2日のアドレスについてのCloudflareのリクエスト分析、1分ごと、パス別、ユーザーエージェント別、そしてすべてのゾーンの横断で、2026年11月3日にクエリ:スキャンの11.1時間前、07:49 UTCに2件のGET。18:56の分から19:17の分まで(22バケット)で10,907件、早朝のGETを含めて10,909件、4つのクエリで同じ合計。最初の1分に29件、ブロック前で最も多い1分(18:57)に637件、最も多い1分(19:07)に1,107件、最後の1分に300件。ブロックの分より前が2,986件(1分あたり平均373.2件)、後が7,445件で配信は0。結果別では、配信1,388件(12.7%)、アプリが拒否1,473件(13.5%、うちGET 418が420件)、オリジンの520が116件(1.1%)、エッジでブロック7,930件(72.7%)。スキャンの最初の1分からブロックまで8.02分。kiradex.appへのGETが368件、ほかのゾーンへの接触はなし。 ↩↩↩↩↩↩↩↩↩↩↩

  15. 著者によるコードの読解。10月30日と11月2日の時点でデプロイされていたこのサイトのCSRFモジュール:タイムスタンプ、ランダムな値、その2つに対するHMAC-SHA256署名から成るトークン。署名が一致し、タイムスタンプが3,600秒未満なら有効。セッション、Cookie、アドレスには結び付かない。2026年11月3日にソースから読み取り。 ↩↩↩↩

  16. 著者のデータ。保存したクエリ結果からスクリプトで作った台帳(484行、各行にその出典と方法)とそのコードブック、2026年11月3日。各ステータスにどの層が応答したかのルール、ファイアウォールイベントは帰属とタイムスタンプを、リクエストのデータセットは件数を与えるというルール、有効なCSRFトークンを証明するのは200だけというルール、そして当日の私のメモと11月3日のクエリが食い違う箇所の表(10月30日のメモは11:58 PDT、Cloudflareが最後の数分を取り込む前に書かれ、フォーム投稿、422、ペイント共有への投稿、その時間帯のリクエストを少なく数えている)を含む。 ↩↩↩↩↩↩↩↩

  17. 著者によるコードの読解。10月30日と11月2日の時点でデプロイされていたこのサイトのフォームのレートリミッター:デフォルトでクライアントのアドレスごと、時間枠ごとにフォーム投稿5件、時計の1分をキーにした60秒の時間枠、カウンターのKVからの読み取り、失敗した読み取りをゼロと数えるインクリメント内部での2回目の読み取り、そのカウントに1を足した値の別個のKVへの書き込み、読み取ったカウントが上限に達していれば429、そしてKVの呼び出しが失敗したときのプロセスごとのメモリー内リストへのフォールバック。2026年11月3日にソースから読み取り。 ↩↩↩↩↩↩↩↩↩↩

  18. 著者のデータ、台帳の補遺。2026年11月3日に私自身のマシンから行った、このサイトのレート制限ルールのプローブ:14:36:49 UTCに、トークンのないPOSTを0.4秒間隔で10件(422が4件、続いて429が6件)。14:38:03 UTCに、すべての項目をそろえて8件(オリジンからの403が3件、続いて「error code: 1015」という本文の429が5件)。14:35から14:40 UTCまでのフォームについてのCloudflareのファイアウォールイベント、14:40:36 UTCに取得:11件で、すべてレート制限ルールによるブロック。 ↩↩↩↩

  19. 著者のデータ。2026年11月2日10:40から11:32 PSTまでの私のモニターのログと、その検出結果とブロックの記録、11月3日に保存:10:57:00 PSTに始まった実行で、検出器の時間枠は18:57:22 UTCに終わり、検出結果なし。その時間枠でのアドレスのリクエストは、1分ごとのデータで29件から666件のあいだ。次の実行は11:02:46 PST。最初の検出結果は11:03:53 PST(クリティカルなリクエストフラッド、アドレスについて数えたリクエストは1,914件)で、スキャンの最初の1分から7.88分後。アドレスは11:04:01 PSTにアカウントのブロックリストに追加。検出結果は全部で6件、すべて日次ダイジェストに畳み込まれ、メールで送られたものはない。モニターは10月30日には動いておらず、10月31日に再稼働させ、トラフィックの検出器はその後に加わった。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  20. 著者の分析。スタジオのサイトの保存された行と1時間ごとのPOSTのデータを時間帯のバケットで照合、2026年11月3日:Torの投稿しかない時間帯に保存された行はない。10行すべてがTor以外の投稿のある時間帯にある。Torの投稿23件のうち22件は保存された行のない時間帯にあり、23件目はアメリカのリード獲得のアドレスと共有する時間帯にある。時間枠内のリード獲得の5行は、そのアドレスが投稿した5つの時間帯にある。リクエストIDによる結合ではなく、時間帯による推論。 ↩↩↩↩↩↩↩

  21. Cloudflare、“Overview”、Turnstileのドキュメントの概要、2026年11月3日14:08:57 UTCに取得。「Cloudflare’s smart CAPTCHA alternative」(CloudflareのスマートなCAPTCHAの代替)。Cloudflareのトラフィックなしでの埋め込み。操作を必要としないJavaScriptのチャレンジと、プルーフ・オブ・ワークとプルーフ・オブ・スペースの説明。Turnstileがフォームの入力内容にアクセスせず、保存も送信もしないという記述。 ↩↩↩↩↩↩

  22. Cloudflare、“Turnstile Privacy Addendum”、最終更新2025年6月18日、2026年11月3日14:08:57 UTCに取得。Turnstileが処理するシグナル、その目的として述べられていること、そしてボット検出の改善のためのデータ管理者としてのCloudflareの役割。すべて引用。 ↩↩↩

  23. 著者の運用ログ(Captain’s Log)、2026年11月2日:私のインフラを動かしているエージェントのセッションへの、それがメールを送っているのかという私の問い。1つの送信元アドレスを共有する3つのメールの流れ。スキャナーのペイロードを運んだメールの「Contact Form:」の件名。そしてスキャナーのフォーム投稿は「ran about nine a minute」(1分あたり約9件で走っていた)という私のメモ。 ↩↩↩↩↩

  24. 著者によるコードの読解。10月30日と11月2日の時点でデプロイされていたこのサイトのお問い合わせハンドラーとフォームのテンプレート:CSRFトークンが埋め込まれたHTMXのフォーム(フォームのパスへのhx-post)。ハンドラーの前にあるリミッターのデコレーター。送信の前にあり、403を返すCSRFチェック。「Contact Form:」と送り手の名前を件名にしたResend経由の送信。受け入れられた送信での成功の断片。そしてFastAPIのデフォルトのステータスである200でエラーの断片を返す2つの失敗の経路(メールの設定がない場合と、送信が失敗したかidのない場合)。ハニーポットのフィールドはない。2026年11月3日にソースから読み取り。 ↩↩↩↩↩↩↩↩↩

  25. 2026年11月3日より前にデプロイされていたスタジオのサイトについての、著者によるコードの読解とリポジトリの履歴:お問い合わせハンドラーのハニーポットの分岐(隠しのwebsiteフィールドが空でなければ成功の断片で応答し、何も保存もメール送信もしない。これはハンドラーの最初のチェックで、アドレスのチェック、挿入、通知機能より前にある)で、2026年6月5日にフォームを作ったコミット以来存在する。隠し入力のtabindex="-1"とaria-hidden。CSRFトークンはない。ベストエフォートの通知機能は2026年9月21日17:46:47 UTCに、「a paying customer’s message sat unread in D1 for a week」(有料顧客のメッセージがD1の中で1週間未読のままだった)というメッセージのコミットで追加。そして匿名のクライアント向けの、プロセスごとのメモリー内レートリミッターで、クライアントのアドレスとクライアントの分類(リミッターがユーザーエージェントとアドレスから割り当てるボットの階層)をキーにし、ヘルスチェックと静的ファイルを除くすべてのパスが対象。2026年11月3日にリポジトリから読み取り。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  26. 著者の運用ログ(Captain’s Log)、2026年10月30日、11:58 PDTに記入:11:46 PDTの私のフラッドの報告、11:48から11:49 PDTの手作業のブロックとレート制限ルール、11:52 PDTの私のテスト、引用のとおり。その件数はCloudflareがまだフラッドの最後の数分を取り込んでいる最中に取られたもので、この記事では代わりに11月3日のクエリを使っている。 ↩↩↩

  27. Cloudflare、“GraphQL API - Limits”、GraphQL Analytics APIのドキュメント、2026年11月3日14:08:57 UTCに取得。適応型データセットは「retain at least 31 days of data on every plan」(すべてのプランで少なくとも31日分のデータを保持する)。 ↩↩

  28. Resend、“Event Types”、ResendのWebhookのドキュメント、2026年11月3日14:14:47 UTCに取得。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。 ↩↩↩

  29. 著者のデータ。8日間のクエリでこのサイトのお問い合わせフォームに投稿した、2つのスキャナーと私のテスト用アドレスを除くすべてのアドレスについてのCloudflareのリクエスト分析、2026年11月3日にクエリ:3つのアドレス(韓国、日本、インド)で、それぞれ10月27日、28日、29日のいずれか1日にちょうど2件の200。 ↩

  30. RIPE NCC、RIPEstatのnetwork-info、whois、as-overview、10月30日のアドレスとそのASについて、2026年11月3日14:10:34と14:11:34 UTCに取得:AS34534、保有者「BULLIONET FBW NETWORKS SAS」、国FRで登録された割り当て。 ↩

  31. 著者のデータ。10月30日のアドレスについて、パス、メソッド、ステータスでグループ化したCloudflareのリクエスト分析、2026年11月3日にクエリ。クエリは3,000行の上限に達し、76,429件中74,125件のリクエストをカバーしたので、パスの件数は下限:異なるパスは少なくとも2,881個、パスにSQLインジェクションのペイロードを含むリクエストは少なくとも2,680件で、異なるパスは少なくとも810個。ペイントの共有パスへのPOSTは320件ですべて拒否(422または400)、8日間でほかのアドレスからはなし。第2節で引用した共有パスのペイロード入りの変種には404が返った。 ↩↩↩↩

  32. 著者のデータ。10月30日のアドレスについて、ユーザーエージェントでグループ化したCloudflareのリクエスト分析、2026年11月3日にクエリ。クエリは500行の上限に達し、76,429件中76,095件のリクエストをカバーしたので、ここでの件数はすべて下限:Windows用の普通のChrome 141の文字列1つを使ったリクエストが少なくとも69,322件、Googlebotを名乗るものが少なくとも2,755件、少なくとも396種類の文字列でSQLインジェクションのペイロードを含むものが少なくとも3,154件。 ↩↩↩↩↩↩

  33. PortSwigger Web Security Academy、“SQL injection cheat sheet”、2026年11月3日14:10:34 UTCに取得。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。 ↩↩↩↩↩

  34. sqlmapプロジェクト、“time_blind.xml”、このツールの時間ベースのブラインドペイロードの定義、2026年11月3日14:12:27 UTCに取得。SLEEP、PG_SLEEP、WAITFOR DELAY、DBMS_PIPE.RECEIVE_MESSAGEのペイロードを含む。ファミリーを示すために引用しているもので、どちらかのスキャナーが使ったツールとしてではない。 ↩

  35. PortSwigger Web Security Academy、“Blind SQL injection”、2026年11月3日14:10:34 UTCに取得。クエリ内の時間遅延がなぜHTTPレスポンスを遅らせるのか、引用のとおり。 ↩

  36. OWASP Automated Threats to Web Applications、“OAT-014 Vulnerability Scanning”、2026年11月3日14:13:41 UTCに取得。概要、引用のとおり。 ↩

  37. 著者のデータ、台帳の補遺。2つのスキャナーのフォームへのPOSTについて、エッジのステータスとオリジンのステータスでグループ化したCloudflareのリクエスト分析、2026年11月3日にクエリ:10月30日は、429の830件のうち395件にオリジンのステータス429があり、残りの435件は記録なし。11月2日は、429の31件のうち16件にオリジンの429があり、15件は記録なし。オリジンのステータスが記録されている429で、エッジだけのステータスを持つものはない。 ↩↩

  38. 著者のテスト、2026年11月3日:サイト自身の環境(FastAPI 0.128.0)で実行した10行のFastAPIアプリで、エンドポイントにリミッター風のデコレーターを付けたもの。必須項目を欠いたPOSTはラッパーを一度も呼ばずに422を返し、完全なPOSTはラッパーに達した。 ↩

  39. 著者のデータ。2026年10月30日の私自身のテスト用アドレスについてのCloudflareのファイアウォールイベントとリクエスト分析、11月3日にクエリ:レート制限ルールのイベント3件(最初は18:52:38 UTC、ブラウザーのユーザーエージェント)と、私のcurlの投稿に対するユーザーエージェントのチャレンジルールからのマネージドチャレンジ24件。インシデントのすべての件数から除外。 ↩↩↩↩↩

  40. RIPE NCC、RIPEstatのnetwork-info、whois、as-overview、11月2日のアドレスとそのASについて、2026年11月3日14:10:34と14:11:34 UTCに取得:AS24875、保有者「NOVOSERVE-AS NovoServe B.V.」、NovoServe B.V.に登録された経路、国NL。下位割り当ての説明には私人の名前があり、ここでは繰り返さない。 ↩

  41. 著者のデータ。11月2日のアドレスについて、ユーザーエージェントでグループ化したCloudflareのリクエスト分析、2026年11月3日にクエリ、完全(73個の文字列で10,909件のリクエストすべてをカバー):Windows用の普通のChrome 141の文字列1つを使ったリクエストが9,725件、Googlebotを名乗るものが942件、149件のリクエストに54種類のSQLインジェクションのペイロード、78件に3種類の引用符を壊す文字列、12件に@@のマーカートークン12個、そして普通のものが2つ。 ↩↩↩↩↩↩

  42. 著者のデータ。台帳のスクリプトが2026年11月3日に作成した両日のユーザーエージェントの表:Windows用の普通のChrome 141の文字列とGooglebotの文字列は、10月30日と11月2日でバイト単位で同一。 ↩

  43. 2026年11月3日に著者が行った再生。11月2日の18:58:23から19:03:23 UTCまでの完全なCloudflareのデータに対して私のモニター自身の検出器を実行したもので、11:02:46 PSTに始まった実行の時間枠にあたる。15:55:04 UTCに実行し、正確なコマンドと検出器の完全な出力とともに再生記録として保存。台帳の補遺の5行は、その記録からスクリプトで導いたもの:リクエストフラッドの検出結果でアドレスからのリクエスト2,143件、スキャナーの検出結果で拒否されたリクエスト1,055件、そして39件の投稿(うち23件が拒否)による高重大度のフォームフラッドの検出結果で、これは日次ダイジェストに送られ、メールもブロックも行わない。ライブの実行の1,914件は、実行がクエリした瞬間に終わる時間枠のもので、私のモニター自身のログが記録した数であり、ライブの実行はフォームフラッドの検出結果を出していない。同じ日に行った時間枠の変更:各ライブの時間枠は現在、実行の90秒前に終わり、前回の時間枠が終わったところから始まる。前回の記録がないか古い場合は、単純に5分さかのぼる。 ↩↩↩

  44. 著者のデータ。11月2日のアドレスについて、パスでグループ化したCloudflareのリクエスト分析、2026年11月3日にクエリ、完全(2,318行で10,909件のリクエストすべてをカバー):異なるパス2,173個、そしてSQLインジェクションのペイロードを含む542個の異なるパスへのリクエスト778件。 ↩↩

  45. OWASP Web Security Testing Guide v4.2、“Testing for SQL Injection”、2026年11月3日14:12:12 UTCに取得。エラーベースと時間遅延の技法、引用のとおり。 ↩

  46. 2026年11月3日の3つ目の変更についての著者の記録。この記事の草稿のレビュー後に行ったもの:16:12 UTCに、両方のウィジェットの許可ドメインをサイトとそのwwwホストに設定し、localhostを削除。両方の検証器のデフォルトのホスト名一覧を同じ2つのホスト名に設定し、ローカル開発では独自の一覧を設定。このサイトのテストはデフォルトでlocalhostが拒否されることを確認(11個合格)。スタジオのサイトを再デプロイし、Cloudflareの常に合格するテスト用キーに対してローカルで実行すると、ダミーのトークンはそのキーのプレースホルダーのホスト名を一覧に入れた場合はバリデーションに達し、入れない場合は拒否された。16:13 UTCに、脅威スコアをキーにした2つのマネージドチャレンジのルール(各ゾーンに1つ)を、その説明に理由を記して無効化。ユーザーエージェントのチャレンジは残す。この変更後のライブチェック:スタジオのサイトは16:15 UTC(ウィジェット、トークンのない投稿とゴミのトークンの投稿は403、ハニーポットは200)、このサイトは16:18 UTC(ウィジェット、トークンのない投稿は403)。どれもホスト名のルールは試していない。 ↩↩↩

  47. Cloudflare、“Custom rules”、Cloudflare WAFのドキュメント、2026年11月3日14:10:34 UTCに取得。カスタムルールが何をするか、引用のとおり。そしてLog以外のすべてのアクションを持つ、Freeプランの5つのルール。 ↩

  48. Cloudflare、“Rate limiting rules”、Cloudflare WAFのドキュメント、2026年11月3日14:08:57 UTCに取得。ルールのパラメーター。精度についての但し書きとカウンター更新の遅延、引用のとおり。提供状況の表で、Freeプランには10秒のカウント期間、10秒の緩和タイムアウト、IPによるカウント、そしてルール1つが与えられる。 ↩↩↩↩↩↩

  49. Ned Batchelder、“Stopping spambots with hashes and honeypots”、2007年1月21日作成、2026年11月3日14:10:34 UTCに取得。ハニーポットの定義、ボットが見えないフィールドに引っかかる理由、再生型とフォーム記入型の分類、そしてボットが対策を破ることとCAPTCHAに戻すことについての彼の2つの文。すべて引用。このページは、この技法を彼が自分のサイトを守る方法として説明しており、彼が発明したとは主張していない。 ↩↩↩↩↩↩

  50. OWASP Cheat Sheet Series、“Credential Stuffing Prevention Cheat Sheet”、2026年11月3日14:10:34 UTCに取得。単一の予測可能な量の制限に頼らないこと。アドレスごとの低い量がブロックリストとレート制限を打ち負かすこと。JavaScriptを実行せずに直接投稿するツール。すべて引用。 ↩↩↩

  51. Cloudflare、“Cloudflare HTTP headers”、Cloudflare Fundamentalsのドキュメント、2026年11月3日14:14:20 UTCに取得。CF-IPCountryヘッダーの特別なコードで、「T1 - Used for clients using the Tor network.」(T1:Torネットワークを使うクライアントに使用)を含む。 ↩

  52. Cloudflare、“IP Access rules parameters”、Cloudflare WAFのドキュメント、2026年11月3日14:14:20 UTCに取得。「T1: Tor exit nodes (country name: Tor)」(T1:Torの出口ノード(国名:Tor))。 ↩

  53. The Tor Project、“Bulk Tor Exit Exporter”、check.torproject.org、2026年11月3日14:10:34 UTCに取得。リストの説明(引用のとおり)と、過去のデータについてCollecTorを案内する記述。リスト自体は同時刻にhttps://check.torproject.org/torbulkexitlist から取得し、1,239個のアドレスを含んでいた。 ↩↩

  54. 著者のデータ、台帳の補遺。8日間のクエリでスタジオのサイトのフォームに投稿したすべての異なるアドレスを、Cloudflareの国コードで分け、2026年11月3日14:10:34 UTCに取得したTor Projectの一括出口リスト(1,239アドレス)とスクリプトで照合したもの:T1とコード付けされた23個のアドレスのうち23個がリストに載り、ほかの12個のうち0個。11月3日のリストだけで、イベント当日はアーカイブと照合していない。 ↩

  55. The Tor Project、“Sources”、Tor Metrics、2026年11月3日14:10:34 UTCに取得。「containing the IP addresses of relays that it found when exiting through them」(それらを経由して出たときに見つけたリレーのIPアドレスを含む)TorDNSELの出口リストと、そのアーカイブ。 ↩↩

  56. OWASP Automated Threats to Web Applications、“OAT-017 Spamming”、2026年11月3日14:10:34 UTCに取得。概要、フォームスパムを含む別名、そして意図しない受信者に向けたフォームからメールへの悪用の除外、引用のとおり。 ↩↩

  57. 著者の分析。スタジオのサイトに保存された108件のメッセージを、固定のルールで、最初に一致したものを採用して分類(価格の質問と賞品の誘い文句は送り手の名前の一致、残りはメッセージの本文、1行は本物の問い合わせとして手作業で分類)、2026年11月3日:第5節の表の件数。価格の質問の言語はテーブル全体で25、10月で9で、固定された各文についての私の読みによる。クロアチア語(またはボスニア語)とインドネシア語(またはマレー語)は微妙な判定。クエリ後の時間枠のすべての行をスパムに分類。 ↩↩↩↩↩

  58. Cloudflare、“Turnstile widgets”、Turnstileのドキュメント、2026年11月3日14:08:57 UTCに取得。マネージド、非インタラクティブ、インビジブルの各モードと、それぞれで訪問者に見えるもの(マネージドは引用)。インビジブルモードのプライバシーポリシーの条件。challenges.cloudflare.comのオリジン。 ↩↩↩

  59. Cloudflare、“Cloudflare Turnstile plans”、Turnstileのドキュメント、2026年11月3日14:08:57 UTCに取得。FreeとEnterprise。最大20個のウィジェット。無制限のチャレンジ。ほかのCloudflareのサービスなしでの利用、引用のとおり。 ↩

  60. Cloudflare、“Validate the token”、Turnstileのサーバー側の検証のドキュメント、2026年11月3日14:08:57 UTCに取得。siteverifyを呼ぶ必要性。偽造されたトークン。エンドポイントとそのsecret、response、remoteipのフィールド。300秒の有効期間と使い捨て。最大2,048文字。actionとhostnameの確認。エラーコードの表と、期限切れについての2つの説明。検証のフラッドに対するレート制限。引用した箇所はすべてこのページから。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  61. Cloudflare、“Cloudflare Turnstile - Easy CAPTCHA Alternative”、製品ページ、2026年11月3日14:08:57 UTCに取得。「Turnstile never harvests data for ad retargeting.」(Turnstileは広告のリターゲティングのためにデータを収集することは決してない)。 ↩

  62. Cloudflare、“Content Security Policy”、Turnstileのドキュメント、2026年11月3日14:08:57 UTCに取得。https://challenges.cloudflare.com に対するscript-srcとframe-src。nonceベースの方式。プリクリアランスのためのconnect-src 'self'の要件、引用のとおり。 ↩↩↩

  63. Cloudflare、“Interstitial Challenge Pages”、Cloudflare Challengesのドキュメント、2026年11月3日14:08:57 UTCに取得。チャレンジページがリクエストをどう保留するか。大半のWAFルールにはマネージドチャレンジを推奨。AJAXとXHR(fetch)の制約と、Turnstileのプリクリアランスの推奨。どちらも引用。 ↩↩↩↩↩↩

  64. Cloudflare、“Detect a Challenge Page response”、Cloudflare Challengesのドキュメント、2026年11月3日14:10:34 UTCに取得。challengeに設定されるcf-mitigatedヘッダーと、すべてのチャレンジのtext/htmlのcontent-type、引用のとおり。 ↩

  65. Cloudflare、“Clearance”、Cloudflare Challengesのドキュメント、2026年11月3日14:10:34 UTCに取得。cf_clearance Cookieとプリクリアランス、引用のとおり:Cookieはデフォルトの使い捨てトークンに加えて発行され、Siteverifyは引き続き必須で、Cookieはウィジェットに設定されたクリアランスのレベルまでチャレンジを回避する。 ↩↩↩

  66. 2026年11月3日の2つ目の変更についての著者の記録。同じ朝に行ったもの:スタジオのサイトは15:19 UTCにデプロイし、15:22 UTCにライブで確認(ウィジェット、スクリプト、サイトキー、ライブのframe-srcにあるチャレンジ用オリジン。トークンのない投稿とゴミのトークンの投稿は403で拒否。ハニーポットの投稿は200で応答)。このサイトは15:20 UTCからビルドし、キャッシュをパージして15:25 UTCにライブで確認(同じページのチェック、トークンのない投稿は403で拒否、続いてライブのページから取った有効なCSRFトークンを持ちTurnstileのトークンを持たない投稿も再び403で拒否。これはデプロイ後もフォームがフェイルクローズであることを示すが、ホスト名のチェックは試せない)。両方の検証器は現在、Cloudflareの応答にあるホスト名がサイト自身のものの1つであることを要求し(ホスト名のない応答は拒否)、Cloudflareがactionを返す場合はウィジェット自身のものであることを要求する。このサイトの11個のテストが合格し、新しいテストはサイトのホスト名を受け入れ、別のホスト名、別のaction、ホスト名の欠落を拒否する。スタジオのサイトでは、プレースホルダーのホスト名を返しactionを返さないCloudflareの常に合格するテスト用キーに対してローカルで実行すると、ダミーのトークンはそのホスト名を一覧に入れた場合はバリデーションに達し、入れない場合は拒否された。 ↩↩↩↩

  67. Cloudflare、“Test your Turnstile implementation”、Turnstileのドキュメント、2026年11月3日14:08:57 UTCに取得。常に合格するテスト用キーと常に失敗するテスト用キー、そしてダミーのトークン。本番のシークレットはダミーのトークンを拒否する。自動テストスイートはボットとして検出される、引用のとおり。 ↩↩↩↩↩

  68. 2026年11月3日の4つ目の変更についての著者の記録。この記事の草稿の2回目のレビュー後に行ったもの:最初のバージョンの、HTMXのbefore-swapイベントのハンドラー(4xxの応答を差し込まないというHTMXのデフォルトに対して403の断片を通すもの)はお問い合わせフォームに付けられていたが、HTMXはそのイベントを差し込みのターゲット、つまりフォームの兄弟要素で発火させるため、403の断片は一度も表示されなかった。レビューは同梱のHTMXとテンプレートを読んでそれを見つけた。修正は応答のターゲットのidに範囲を限定したページのbody上のリスナーで、トップページのテストがそれが描画されることを確認する(11個合格)。16:18 UTCにデプロイを確認した後、16:21 UTCにPlaywrightのChromiumでブラウザーのチェックを行い、ウィジェットの730文字のトークンをゴミの文字列に置き換え、必須項目をプレースホルダーの値で埋め、HTMXを通じてフォームを送信した。応答のターゲットにはエラーの断片「error: Verification failed. Reload the page and try again.」が表示され、成功の断片はなく、何も送信されなかった。未実施:人間によるエンドツーエンドの送信。 ↩↩

  69. Cloudflare、“Embed the widget”、Turnstileのドキュメント、2026年11月3日14:08:57 UTCに取得。api.jsのスクリプト、ウィジェットのコンテナ、フォームの中に作られる隠しのcf-turnstile-response入力、そしてタイムアウトまたは期限切れのウィジェットのためのturnstile.reset(widgetId)、引用のとおり。 ↩↩↩

  70. 2026年11月3日に、それぞれ自身のサイトのデプロイが成功した後、そしてキャッシュのパージの後に行ったライブチェックについての著者の記録:06:35:24と06:35:54 PSTに、各トップページに1つのウィジェットのdiv、1つのscriptタグ、サイトキーがあり、各ライブのframe-srcにチャレンジ用オリジンがあった。このサイトでは、すべての項目をそろえゴミのCSRFトークンを付けた投稿が、CSRFチェックにより403で拒否された。14:43 UTCに、このサイトで、ライブのページから取った108文字の有効なCSRFトークンを持ちTurnstileのトークンを持たない投稿が「Verification failed. Reload the page and try again.」とともに403で拒否され、同じ投稿にゴミのTurnstileトークンを付けたものも403で拒否された。スタジオのサイトでは、トークンのない投稿とゴミのトークンの投稿が403で拒否され、ハニーポットの投稿は200で応答し、何も保存もメール送信もされなかった。14:39:50と14:40:04 UTCにPlaywrightのChromiumで、ウィジェットが描画され(602×73ピクセルと557×71ピクセル)、TurnstileのAPIオブジェクトが読み込まれ、両方のページで隠しの応答入力が6秒以内に730文字のトークンを保持した。未実施:人間によるエンドツーエンドの送信。 ↩↩↩

  71. Cloudflare、“Rate limiting parameters”、Cloudflare WAFのドキュメント、2026年11月3日14:08:57 UTCに取得。パラメーターの名前。Free、Pro、Businessプランではチャレンジのアクションが常にスロットリングになること(要約)。チャレンジに合格するとカウンターがリセットされること(引用)。 ↩

  72. OWASP Cheat Sheet Series、“Authentication Cheat Sheet”、2026年11月3日14:10:34 UTCに取得。多層防御の制御としてのCAPTCHA、引用のとおり。 ↩

  73. Resend、“Retrieve Sent Email”、ResendのAPIリファレンス、2026年11月3日14:14:47 UTCに取得。取得したメールのlast_eventフィールド、たとえば「delivered」。 ↩↩

  74. Resend、“View and manage emails”、Resendのドキュメント、2026年11月3日14:15:12 UTCに取得。「sent」と「delivered」を含むメールのイベント一覧(引用)、そしてBroadcastsとBatchesだけに使われる「queued」。 ↩

  75. Cloudflare、“Onion Routing and Tor support”、Cloudflare Networkのドキュメント、2026年11月3日14:08:57 UTCに取得。すべてのプランでのOnion Routingと、人間のTorユーザーとボットの区別、引用のとおり。 ↩

  76. Cloudflare、“Challenges”、Cloudflare Challengesのドキュメント、2026年11月3日14:08:57 UTCに取得。「Cloudflare does not use CAPTCHA puzzles or visual tests like selecting objects or typing distorted characters.」(CloudflareはCAPTCHAのパズルや、物体を選んだり歪んだ文字を入力したりする視覚的なテストを使わない)。 ↩

  77. OWASP Community、“Blind SQL Injection”、2026年11月3日14:10:34 UTCに取得。データベースの一時停止に依拠する時間ベースのブラインドインジェクションで、例としてWAIT FOR DELAYとpg_sleep()。 ↩

関連記事

アラート疲れ:セキュリティモニターから1日82通のメール

1日1通のダイジェストで、セキュリティモニターのメールは1日82通から翌日19通に。そのポリシー、アラートメール0通でブロックされたスキャナー、それでも19通届いた理由。

52 分で読める

フォークボムに救われた

LiteLLMを攻撃した人物は、実装をひとつ間違えました。47,000件のインストールが46分で発覚したのは、その間違いだけが理由でした。

8 分で読める

私が書かないと決めていること

ブログクラスターの声は、何を出版するかではなく、何を出版しないかから生まれます。カテゴリー的拒否、パターン的拒否、興味深い拒否のそれぞれが、クラスターの姿を形作るのです。

13 分で読める