Cloudflare Turnstileはなぜボットを見破るのか:CAPTCHAなき認証の仕組み解説
TL;DR
Cloudflare TurnstileはCAPTCHAの代替として、ユーザーに何も操作させずにボットを検知する。Proof-of-Work・Proof-of-Space・Web APIプローブ・行動分析の4つのチャレンジをバックグラウンドで実行し、機械学習で総合判定してトークンを発行する。CDP(Chrome DevTools Protocol)経由の自動操作は環境フィンガープリントで即座にブロックされ、同一セッションでの復帰は困難(ただし公式にはリトライやリセット機構が存在する)。
きっかけ:フォーム自動入力で壁にぶつかった
Chrome DevTools MCP経由であるWebフォームを自動入力していたら、Submitボタンが永久にdisabledのままになった。JSで無理やりbtn.disabled = falseしてクリックしたら「Cloudflare Turnstile verification failed」。リロードしても、手動クリックしても、もうそのブラウザセッションでは二度と送信できなかった。
なぜ自動化ツールが検知されるのか。そもそもTurnstileはどういう仕組みで動いているのか。調べてみたら、想像以上に多層的な防御だった。
CAPTCHAの何が問題だったのか
従来のCAPTCHAは「歪んだ文字を読め」「信号機を選べ」のような視覚パズルで人間を証明させていた。問題は明確だ。
- ユーザー体験が最悪:毎回パズルを解かされるストレス
- アクセシビリティの壁:視覚障害者にとって事実上の排除
- ボットが解ける:機械学習の進歩で画像認識CAPTCHAの突破率は上がり続けている
- 離脱率の増加:CAPTCHAの存在だけでフォームの離脱率が最大40%増加するという報告もある
Cloudflareの答えは「そもそもユーザーに何もさせない」だった。
Turnstileの4層チャレンジ
Turnstileは目に見えないチャレンジを複数同時に実行する。訪問者のリスクレベルに応じて難易度が自動調整される。
注意: 以下のチャレンジ構成はリバースエンジニアリング系の技術分析に基づいており、Cloudflareは内部実装の詳細を公式には公開していない。実装は予告なく変更される可能性がある(2026年2月時点の情報)。
flowchart LR
V["訪問者がページを開く"] --> W["Turnstile JS<br/>ウィジェット読み込み"]
W --> C1["Proof-of-Work"]
W --> C2["Proof-of-Space"]
W --> C3["Web APIプローブ"]
W --> C4["行動分析"]
C1 --> ML["機械学習モデル<br/>リスク判定"]
C2 --> ML
C3 --> ML
C4 --> ML
ML -->|低リスク| T["トークン発行"]
ML -->|高リスク| B["ブロック / 追加検証"]
1. Proof-of-Work(計算証明)
ブラウザに小さな計算タスクを解かせる。CPU負荷を測定しているのではなく、計算コストを課しているのがポイントだ。
原理はBitcoinのマイニングと同じで、「SHA-256(nonce + challenge)の先頭Nビットがゼロになるナンスを見つけろ」的な問題を出す。ブラウザはナンスを総当たりで試すしかない(解くのはO(2^N))が、サーバー側の検証はハッシュ1回で済む(O(1))。この非対称性が設計の核だ。
正規ユーザーには難易度Nが低く設定されるので数ms〜数百msで終わる。一方、ボットファームが1万セッション並列で回すと1万回分のPoWが必要になり、CPUコストが線形に積み上がる。「1台なら安いが大量にやると割に合わない」という経済的抑止力だ。副次的に、解答速度が異常に速い場合は「ブラウザ以外の高性能環境で解いている」という推測材料にもなりうる。
2. Proof-of-Space(メモリ証明)
Proof-of-Workがボットに「CPU時間」を課すのに対し、Proof-of-Spaceは「メモリ」を課す。CloudflareはTurnstileの発表時にこのチャレンジタイプの存在を公表しているが、具体的なアルゴリズムやメモリサイズは開示していない。
分かっているのは、ブラウザにメモリを確保させ、その確保に紐づいた計算を行わせるという点だ。正規ブラウザにとっては問題にならない程度のメモリでも、ボットファームが1台のサーバーで数千セッションを並列実行する場合、セッションごとにメモリが必要になり、並列数に物理的な上限がかかる。
用語について: 学術的にはProof-of-Spaceはディスクストレージの証明(Ateniese et al., Dziembowski et al.)を指すことが多い。CloudflareがTurnstileで「Proof-of-Space」と呼んでいるのはRAM確保チャレンジであり、暗号論文の文脈とは異なる。
3. Web APIプローブ
ここが最も巧妙だ。ブラウザが本物かどうかを、ブラウザが持つAPIの挙動で判定する。
具体的には以下のようなシグナルを見ている。
- Canvas/WebGL フィンガープリント:同じ描画命令に対する出力がブラウザ・GPU・ドライバの組み合わせごとに微妙に異なる
- navigator オブジェクトの整合性:
navigator.webdriverがtrueになっていないか、navigator.pluginsの配列が空でないか - Performance APIのタイミング:ページ読み込みのタイミングパターンが人間の操作と一致するか
- CDP検知:
Runtime.enableやPage.enableなどのDevToolsプロトコルコマンドが実行された痕跡
最後の項目が今回ハマった原因だ。Chrome DevTools Protocol経由でページを操作すると、ブラウザのランタイムに検知可能な痕跡が残る。window.chrome.csiの挙動変化、Error.stackのフォーマット差異、DevToolsが注入するバインディングオブジェクトなど、複数のシグナルでCDP制御を検知する。
4. 行動分析
マウスの動き、スクロールパターン、キー入力のタイミング、ページ遷移のパターンなどを総合的に分析する。人間の操作には特有の「揺らぎ」がある。完璧に等間隔でクリックする人間はいないし、マウスの軌跡が完全な直線になることもない。
ここで重要なのは、単一のシグナルでは判定しないということだ。すべてのチャレンジの結果を機械学習モデルに投入し、総合スコアでリスクを判定する。
トークンの発行と検証フロー
チャレンジに合格すると、TurnstileはJSON Webトークン的な検証トークンを発行する。このトークンはフォームの送信時にサーバーに渡される。
sequenceDiagram
participant B as ブラウザ
participant T as Turnstile JS
participant CF as Cloudflare
participant S as サイトのサーバー
B->>T: ページ読み込み(Sitekeyで初期化)
T->>CF: チャレンジ取得
CF-->>T: チャレンジ配信
T->>T: 4層チャレンジ実行
T->>CF: チャレンジ結果送信
CF-->>T: 検証トークン発行
Note over T: トークンをhidden inputにセット
B->>S: フォーム送信(トークン付き)
S->>CF: Siteverify API でトークン検証
CF-->>S: 検証結果(成功/失敗)
S-->>B: レスポンス
サーバー側の検証はhttps://challenges.cloudflare.com/turnstile/v0/siteverifyへのPOSTリクエストで行う。Secret Keyとトークンを送ると、有効性・発行時刻・ホスト名の一致を検証してくれる。
トークンには有効期限がある。Pre-Clearance機能を使えば、セッション内でクッキーベースの再利用が可能だが、基本的には一度きりだ。
3つのウィジェットモード
Turnstileは用途に応じて3つのモードを提供している。
| モード | ユーザーに見えるか | インタラクション | 用途 |
|---|---|---|---|
| Managed | リスクに応じて表示 | 必要な場合のみチェックボックス | 汎用(推奨) |
| Non-interactive | 見えない | 一切なし | UX最優先 |
| Invisible | 見えない | 一切なし | 完全非表示 |
Managedモードが最も柔軟で、Cloudflareが自動的にリスクを判断して、怪しいときだけチェックボックスを出す。大半のユーザーは何も見ないまま通過する。
なぜCDP制御ブラウザは復帰不可能なのか
今回最も興味深かったのはこの挙動だ。一度CDPで接続したブラウザセッションでは、筆者の環境ではリロードしても手動操作に切り替えてもTurnstileのチャレンジに合格できなかった。
注意: 以下はmacOS + Chrome + Chrome DevTools MCP環境での筆者の観測に基づく。ブラウザのバージョンやOS、Turnstile側のアップデートによって挙動が異なる可能性がある。なお、Cloudflareの公式ドキュメントではチャレンジの自動リトライや
turnstile.reset()によるリセット機構が案内されており、理論上は復帰パスが存在する。
考えられる理由はこうだ。
- ブラウザ起動時のフラグ検知:
--remote-debugging-port等のフラグで起動された時点で、ブラウザのランタイム環境に検知可能な変更が入る - セッション単位の信頼スコア:Turnstileはページ単位ではなくセッション単位で信頼を管理していると推測される。「自動化環境」と判定されたセッションでは信頼スコアが低いままになりやすい
- Turnstileトークンの前提条件:トークン発行の前提として「信頼できる環境からのチャレンジ結果」が必要。環境自体の信頼スコアが低ければ、チャレンジ結果が正しくてもトークンが発行されにくい
stateDiagram-v2
[*] --> Clean: 通常のブラウザ起動
[*] --> Tainted: CDP付きで起動
Clean --> Verified: チャレンジ合格
Verified --> TokenIssued: トークン発行
Tainted --> Blocked: チャレンジ拒否
Blocked --> Blocked: リロード/手動操作(筆者環境では復帰せず)
Blocked --> Retry: turnstile.reset()(公式にはリトライ可能)
Retry --> Blocked: 環境シグナルが変わらなければ再失敗
筆者の環境では、「自動で入力してから手動でSubmitだけ押す」という戦略は機能しなかった。ただし、これはTurnstileが復帰を一切許さないという意味ではなく、CDP制御下の環境シグナルが継続的にチャレンジ失敗を引き起こしていたためだと考えられる。
実装側から見たTurnstile
ウェブサイトにTurnstileを組み込む側の視点も見ておこう。実装は驚くほど簡単だ。
クライアント側はscriptタグ1行とdiv要素1つ。
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAA..."></div>
サーバー側はトークンの検証だけ。
import requests
def verify_turnstile(token: str, secret_key: str) -> bool:
resp = requests.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={"secret": secret_key, "response": token}
)
return resp.json().get("success", False)
SPAの場合はturnstile.render()を明示的に呼ぶ。
const widgetId = turnstile.render('#turnstile-container', {
sitekey: '0x4AAAAAAA...',
callback: (token) => {
// トークンをフォームデータに含めて送信
document.getElementById('cf-token').value = token;
},
});
たったこれだけで、先ほどの4層チャレンジが全部裏で動く。Cloudflareのネットワークを経由しなくても動作するので、既存のインフラをそのまま使える。
CAPTCHAとの比較
| 観点 | 従来のCAPTCHA | Turnstile |
|---|---|---|
| ユーザー操作 | パズル解答が必要 | 不要(大半のケース) |
| 判定方法 | パズルの正答率 | 環境+行動の総合スコア |
| ボット耐性 | 画像認識で突破可能 | 多層シグナルで高耐性 |
| アクセシビリティ | 視覚障害者に困難 | 操作不要のため問題なし |
| プライバシー | 広告トラッキングに利用されがち | トラッキングデータを収集しない |
| 離脱率への影響 | 大きい | 最小限 |
まとめ
Turnstileの設計思想は明快だ。「人間であることの証明」を、ユーザーの負担からシステムの責務に移す。
4つのチャレンジ(Proof-of-Work、Proof-of-Space、Web APIプローブ、行動分析)を組み合わせた多層防御は、単一のシグナルに依存しないため堅牢だ。そして一度「信頼できない環境」と判定されたセッションは回復しないという設計は、自動化ツールによる段階的な突破を構造的に防いでいる。
個人的には、CDP検知の粘着性が一番印象的だった。「入力だけ自動化して送信は手動で」が(少なくとも筆者の環境では)通用しなかったのは、ボットと人間の境界線を「操作」ではなく「環境」で引いているからだ。公式にはリトライ・リセット機構があるものの、環境シグナル自体が汚染されている限り実質的に復帰は困難だった。
ちなみに今回のフォーム送信は、結局普通のSafariで開いてテキストファイルからコピペした。自動化の限界を身をもって体験するいい機会だった。
以上。
References
- Cloudflare Turnstile 公式ドキュメント - チャレンジの種類、ウィジェットモード、Pre-Clearance、Siteverify APIの仕様
- Cloudflare Turnstile: What is that and how to solve it - Turnstileの内部動作、CDP検知メカニズム、フィンガープリント手法の技術的分析
- Fighting API Bots with Cloudflare’s Invisible Turnstile - Troy HuntによるTurnstileの非インタラクティブチャレンジの実装事例
- Defeat Cloudflare Turnstile - Turnstileのリスク評価とチャレンジ難易度調整の仕組み