
こんにちは。Osukeラーニング、運営者の「osuke」です。
問い合わせformへ営業spamが増え、「画像を何枚も選ばせるCAPTCHAは利用者を離脱させそう」「Cloudflare Turnstileなら無料で簡単らしいが、widgetを置くだけでよいのか」と迷っていませんか。Turnstileは導入しやすい一方、client側の表示だけで送信を許可すると、攻撃者はform endpointへ直接requestを送り、検証を迂回できます。
結論からいうと、初めてならManaged modeでwidgetを作り、production hostnameを限定し、formへ埋め込み、backendからSiteverifyを呼ぶところまでを1セットにします。さらにrate limit、honeypot、入力validationを重ね、失敗時の再試行と問い合わせ代替手段を用意すれば、spamを減らしながら本物の利用者を置き去りにしにくくなります。
この記事はosuke本人の導入体験としては書かず、2026年8月29日時点のCloudflare公式資料12件と、Xで公開された8件の導入・移行・spam減少・無限loop・端末負荷事例を照合しました。Xの結果は各投稿者の環境に限られるため、仕様は公式資料を優先します。Cloudflare全体が初めてならCloudflareの無料範囲と機能の全体像から確認してください。
記事のポイント
- 最初のwidgetはCloudflare推奨のManaged modeを選ぶ
- sitekeyは公開用、secret keyはbackendだけで保管する
- tokenは5分・1回限りなのでSiteverifyを送信直後に実行する
- spam対策はrate limitやhoneypotと組み合わせて完成させる
Cloudflare Turnstileの仕組みと導入前の判断
Turnstileの設定画面を開く前に、何を防げて何が残るのかを分けます。無料という理由だけで導入すると、backend検証の漏れや本物の利用者が送信できない問題を見逃すため、mode、上限、重ねる対策、失敗時の逃げ道を先に決めます。
TurnstileはCAPTCHA操作を減らすbot対策
Cloudflare公式のTurnstile概要では、visitorのbrowser環境へ小さな非対話challengeを実行し、人間か自動trafficかを判定すると説明しています。画像の信号機や横断歩道を何度も選ばせる方式ではなく、Managed modeでは追加確認が必要なvisitorだけcheckboxを操作するため、form送信前の負担を抑えやすい設計です。
TurnstileはCloudflare CDNを通していないsiteにも埋め込めます。つまり、hosting先が別会社でも、Cloudflare accountでwidgetを作り、許可hostnameとkeyを設定すれば利用できます。XではCHiSA氏がreCAPTCHAからCloudflare TurnstileとContact Form 7の構成へ変更したと投稿しており、既存WordPress formの移行先として使われている具体例です。
効果の公開例として、otsune氏はreCAPTCHA時代に毎日のように届いた営業メールがTurnstile変更後に激減したと投稿しています。ただし手動の問い合わせは残ったとも述べています。Turnstileが判定するのは自動送信の疑いであり、商品を売り込みたい人が手で送る内容や、正規browserを操作する高度なautomationを必ず止めるものではありません。
- 問い合わせ・資料請求・予約form
- account作成・login・password reset
- comment・review・waitlist登録
- checkoutやcoupon取得前のbot対策
無料枠は20 widgetsと無制限challenge
Cloudflare公式のTurnstile plansによると、Free planは月額0で、1 accountにつき最大20 widgets、challenge回数は無制限です。個人blog、小規模事業、開発・test環境だけでなく、多くのproduction applicationにもFreeが想定されています。request数が増えた瞬間に従量課金される料金表ではありません。
無料でも制約はあります。1 widgetへ登録できるhostnameは最大10件、Analyticsのlookbackは最大7日です。Enterpriseはwidgets無制限、1 widget最大200 hostnames、Analytics最大30日などへ広がります。20 widgetsを「20 pages」と誤解しないでください。通常は「contact form用」「login用」のように目的単位でwidgetを分け、同じwidgetを許可hostname内の複数pageで使えます。
公式Hostname managementでは、example.comを登録するとそのsubdomainも許可されます。一方、URL scheme、port、path、wildcardはhostname欄へ入れられません。productionのroot domainだけで足りるなら広げすぎず、stagingやlocalhostはtest keyへ分離すると、流出したsitekeyの無断利用と設定事故を減らせます。
| 項目 | Free | 実務での判断 |
|---|---|---|
| 料金 | 無料 | challenge数による従量課金なし |
| Widgets | 最大20 | form用途・環境ごとに分ける |
| Hostnames | 1 widget最大10 | productionだけを許可する |
| Analytics | 最大7日 | 週1回以上確認・必要なら外部log保存 |
- widgetをform用途別に分ける
- productionとtestのkeyを混ぜない
- hostnameは実際に使うdomainだけ許可する
- 7日を超える傾向は自社logでも残す
Managed・Non-interactive・Invisibleを選ぶ
公式Widget modesにはManaged、Non-interactive、Invisibleの3種類があります。初回はCloudflareが推奨するManagedを選ぶのが安全です。visitorのriskに応じて非対話処理とcheckboxを切り替えるため、「誰にも操作させない」ことより、誤判定時に本人が確認できる余地を残せます。
Non-interactiveはspinnerを表示しますがvisitorへ操作を求めず、Invisibleはwidget自体を見せません。見た目だけならInvisibleが魅力的でも、処理中か失敗かを利用者が判断しにくくなります。決済、password reset、自治体formのように送信失敗のcostが大きい場面では、Managedまたは見える状態表示にして、error messageと再試行buttonを用意した方が運用しやすくなります。
公式Widget configurationsでは、通常300×65px、幅へ追従するflexible、150×140pxのcompactを選べます。appearanceは常時表示、challenge開始時、interactionが必要な時だけの3方式です。mobileのform幅が300px未満ならoverflowし得るため、公開前に実機幅、dark mode、browser zoom 200%でも送信buttonと重ならないか確認します。
| Mode | Visitorに見えるもの | 向く場面 |
|---|---|---|
| Managed | 必要時のみcheckbox | 初回導入・一般form |
| Non-interactive | 処理中spinner | 操作を求めず状態は見せたい |
| Invisible | 原則何も表示しない | 独自の状態・error UIを作れるapplication |
- 迷ったらManaged
- 処理中表示が必要ならNon-interactive
- Invisibleはerror UIを自作できる場合だけ
- mobileはflexibleまたはcompactを実機確認する
Turnstile単体ではspam対策は完成しない
Turnstileが成功したrequestをすべて安全とみなすと、別の穴が残ります。人間が送る営業、1人が何百回も繰り返すrequest、使い捨てemail、同じreferralの自己招待、payloadを巨大化した攻撃は、それぞれ判定軸が違います。Turnstileは「browserが人間らしいか」を見る一層であり、business ruleのvalidationまでは代行しません。
Xでは、Vlad Gerasimchuk氏が公開waitlistへ6種類のanti-abuseを実装し、per-IP rate limit、disposable-email block、honeypot、self-referral block、Turnstile、referral capを組み合わせたと投稿しています。目的ごとに防御を分ける考え方は、Turnstileを突破した1 requestがそのまま無制限処理へ進む事故を防ぎます。
問い合わせformなら、IPまたはaccount単位の送信回数、同一本文の連投、URL数、禁止文字数、存在しないhidden fieldへの入力を記録します。loginならTurnstileに加えてpassword試行回数と一時lockが必要です。Cloudflare側のWAFやrate limitingも含めた入口対策は、Cloudflareのセキュリティ設定で全体像を確認できます。
- Turnstileでbrowser signalを検査
- rate limitで短時間の連投を制限
- server validationで入力値とbusiness ruleを確認
- logと通知で突破・誤検知を発見する
誤検知とbrowser負荷へ逃げ道を用意する
摩擦が小さいTurnstileでも、すべてのvisitorが同じ速さで通過するわけではありません。古いbrowser、privacy extension、社内network、端末時計のずれ、途中cache、challenges.cloudflare.comの遮断でiframeやchallengeが失敗します。重要formで送信buttonを無効のままにすると、本物の問い合わせをspamと同時に失います。
Xでは希氏がFirefox更新後にTurnstileの無限loopが解消したと投稿しています。また、Bradley Erickson氏は直近の個別例でbrowser processが812MBのRAMを使ったと指摘しました。812MBは一般的な消費量ではなく本人環境の観測ですが、proof-of-workが端末resourceを使う以上、低性能端末やloopを無視しない材料になります。
CivicTechAkitaは自治体・地域DXで住民へstressを与えにくい防御策としてTurnstileを紹介しています。公共性や成約価値が高いformでは、error callbackへ「browser更新・extension一時停止・別browser」の案内を出し、電話やemailなど代替経路も残します。利用者を責める文言ではなく、「認証を再読み込みできませんでした」と次の行動を示します。
- 失敗理由を人間向けの文で表示する
- widget resetと再試行buttonを置く
- browser更新・extension・時計・networkを案内する
- 高価値formは電話やemailの代替経路を残す
Cloudflare Turnstileの使い方と安全な実装
ここからはwidget作成、client埋め込み、server検証、test、監視の順に進めます。sitekeyを表示できただけでは完成ではありません。攻撃者がform画面を通らずendpointへ直接送る前提で、backendの許可条件を最後まで作ります。
Dashboardでwidgetと2種類のkeyを作る
公式Dashboard手順に沿い、Cloudflare DashboardのTurnstileから「Add widget」を選びます。Widget nameは「production-contact」「production-login」のように目的と環境が分かる名前にし、実際に表示するhostnameを登録し、最初はManaged modeを選んで作成します。CloudflareへdomainのDNSを移していなくても利用できます。
作成するとsitekeyとsecret keyが発行されます。sitekeyはHTMLへ置く公開識別子で、browserから見えて問題ありません。secret keyはSiteverifyを呼ぶbackend用で、HTML、client JavaScript、Git repository、screenshotへ入れてはいけません。WorkersならSecret、hosting serviceならenvironment variableへ保存し、権限を持つ担当者も最小限にします。
WP Ultimate Securityはsite keyとsecret keyを設定し、login、registration、password reset、checkoutを個別に有効化する流れを紹介しています。製品投稿なので効果保証には使いませんが、WordPressでは問い合わせformだけでなく複数の入口がある点は重要です。保護対象と未対応formを一覧にして、設定漏れを送信testで見つけます。
- 名前にproduction・stagingと用途を入れる
- 許可hostnameを画面保存する
- sitekeyはclient、secretはbackendへ分離する
- secretをrepositoryや記事へ貼らない
静的formとSPAで埋め込み方法を分ける
公式Client-side renderingでは、page読込時から存在する静的formにはimplicit rendering、modalやSPAで後から作るformにはexplicit renderingを案内しています。静的formはAPI scriptとcf-turnstile classのcontainerを置くだけで始められ、form内ならcf-turnstile-responseというhidden inputが自動生成されます。
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form method="post" action="/contact">
<!-- name・email・message fields -->
<div class="cf-turnstile" data-sitekey="YOUR_PUBLIC_SITEKEY"></div>
<button type="submit">送信</button>
</form>
SPAでroute遷移やmodal表示のたびにformを作る場合は、explicit renderingでcontainer生成後にturnstile.render()を呼び、form破棄時にwidgetも整理します。同じcontainerへ二重renderすると送信不能や古いtokenの原因になります。success、error、expired、timeoutのcallbackを用意し、送信中、再試行、失効をUIへ反映してください。
Content Security Policyを使うsiteでは、Turnstileがhostされるchallenges.cloudflare.comへのscript・frame・connectを許可する必要があります。Xではishan氏がlogin状態を再利用するbrowser agentでも厳しいsiteではTurnstile challengeが出ると投稿しており、cookieだけではなくbrowser挙動を含む判定が利用者体験へ影響する例です。自動E2E testはproduction widgetで判定させず、後述のdummy keyを使います。
- 静的HTML formはimplicit
- SPA・modal・動的formはexplicit
- successだけでなくerror・expired・timeoutも処理
- CSPでchallenges.cloudflare.comを遮断しない
BackendからSiteverifyを必ず実行する
公式Server-side validationは、Siteverify APIの呼び出しを必須と明記しています。Client widgetはtokenを発行するだけで、攻撃者は画面を省略してform endpointへ任意文字列を送れます。Backendは受け取ったcf-turnstile-responseを、secret keyと一緒にCloudflareへPOSTし、success:trueを確認してから本処理を実行します。
POST https://challenges.cloudflare.com/turnstile/v0/siteverify
Content-Type: application/json
{
"secret": "SERVER_ENV_TURNSTILE_SECRET",
"response": "TOKEN_FROM_FORM"
}
Tokenは発行から300秒、最大2048文字、1回限りです。送信を二重clickしたり、同じtokenをretryへ使ったりするとtimeout-or-duplicateになります。まずtokenの存在と長さを確認し、Siteverifyへ短いtimeoutを設定し、Cloudflare側の一時errorとinvalid tokenを分けます。通信失敗時に無条件で許可するfail-openはspam入口になるため、重要処理は再試行を促すfail-closedが基本です。
successだけでなくresponseのhostnameが自siteか、widgetへ設定したactionがcontactやloginなど期待値と一致するかも検査します。検証後にDB保存、email送信、account作成へ進めば、無効requestで外部API costを発生させません。Cloudflare Workersでbackendを作る場合は、Cloudflare Workersの始め方も参照してください。
- tokenが存在し2048文字以内
- successがtrue
- hostnameが許可domain
- actionがform用途と一致
- 検証後にだけDB・mail・外部APIを実行
reCAPTCHA移行とdummy key testを行う
公式reCAPTCHA migrationでは、互換移行はreCAPTCHA v2までとされています。api.js?compat=recaptchaを使うとimplicit rendering、g-recaptcha-response、grecaptcha APIへ合わせやすくなりますが、serverの検証先はCloudflare Siteverifyへ変更しなければなりません。TurnstileのSiteverifyはGET queryではなくPOSTだけを受け付けます。
WordPress pluginで「keyを貼ったら完了」と表示されても、問い合わせ、login、password reset、comment、checkoutを別々に送信testします。旧reCAPTCHA scriptがcacheやthemeへ残ると二重読込になり、privacy policyやCSPの記述も古いままです。移行日は旧keyをすぐ削除せず、rollback手順と変更前screenshotを残し、正常送信とspam拒否を確認してから整理します。
公式Testing資料には、常に成功、常に失敗、interactive challenge、duplicate tokenを再現するdummy sitekeyとsecret keyがあります。PlaywrightやCypressなどのautomationはbotと判定され得るため、production keyでE2E testを安定させようとしません。development、test、productionでkeyを分離し、production secretがdummy tokenを拒否することも確認します。
- 正常tokenで1回だけ送信できる
- 失敗tokenで本処理へ進まない
- 5分超過・二重送信で再challengeになる
- Siteverify停止時に無条件許可しない
error codeとAnalyticsで公開後を監視する
公式Error codesでは、110100はinvalid sitekey、110200はdomain未許可、110600はchallenge timeout、200500はiframe load errorです。300*や600*はgeneric challenge failureで再試行対象になり得ます。画面へ番号だけ出さず、運用logへcode、browser、hostname、action、発生時刻を残します。
Turnstile Analyticsでは、hostname、browser、country、user agent、OS、ASN、source IPなどの偏りを確認できます。Solve rateの平均だけを見ると、一部browserで全滅していても気づけません。Free planのlookbackは最大7日なので、公開初週は毎日、その後も週1回、challenge outcomeとSiteverifyのvalid・invalid比率を確認します。
導入完了の基準はwidgetが見えることではなく、「8つの経路で正常送信でき、invalid requestが本処理へ進まず、失敗者が回復できる」状態です。対象formをPC・mobile、Chrome・Firefox・Safari系、通常・privacy extension環境で確認し、問い合わせ担当へerror時の案内を共有します。Turnstileを導入した日、widget名、hostname、key保管先、rollback手順を運用記録へ残してください。
- Managed widgetとproduction hostnameを設定した
- secretをbackendの環境変数へ保存した
- Siteverifyでsuccess・hostname・actionを確認した
- rate limit・honeypot・入力validationを重ねた
- dummy keyで成功・失敗・duplicateをtestした
- error callback・再試行・代替連絡先を用意した
- Analyticsとserver logを確認する担当を決めた
今日の1ステップは、Cloudflare Dashboardでいきなりproduction widgetを公開することではありません。まず保護対象formを一覧にし、「問い合わせ」「login」「account作成」のどこへTurnstile、rate limit、server validationを置くか紙へ書き出してください。その設計図ができれば、keyの貼り間違いと保護漏れを減らした状態で実装を始められます。


