← ブログに戻る

AIクローラーは通すbot対策、WAF2段の設計

この記事を含む総合ガイド LLMO 総合ガイド

GA4を開いたら、直近28日のセッションが1,834でした。前の28日は693です。2.6倍。よくやった、と思いました。

国別に割って、5秒で撤回しました。

前提: 見分け方は前編に書きました

GA4の国別レポートで、シンガポールが682セッション・エンゲージ6・平均滞在0.6秒。682のうち681が desktop / Chrome の一点に固まり、着地先は書籍ページを端から均等に踏む形でした。セッションの37%がこれです。

この現象の分解は前編のアクセス2.8倍の正体、クローラー3種の見分け方に書きました。GA4がUser-Agentを持っていないこと、Cloudflareのログで3種類(AI検索・名乗らないもの・攻撃スキャン)に割れること、AI系は全部UAに自分の名前を書いていること。見分け方はそちらです。この記事は、見分けたあとに何を設定するかの話になります。

GA4 28日の国別セッションとエンゲージセッション。シンガポールは682セッション中エンゲージ6で平均0.6秒、日本は387中222で184.5秒、ブラジルは151中88で200.2秒

セッションの棒とエンゲージの棒を重ねると、並び順が入れ替わります。セッションで数えるとシンガポールが1位、エンゲージで数えると日本とブラジルしか残りません。設定を決めるのに要る数字だけ、この先に置き直します。

止めたくないものが、すでに読者を連れてきている

ここで手が止まりました。私はLLMOの本を書いていて、サイト自体をその実装リファレンスにしています。AIに引用されるための前提は、AIにクロールされることです。

冒頭で触れた数字を、チャネル別に並べ直します。

チャネルセッションエンゲージエンゲージ率平均滞在
Direct1,064999.3%45.9秒
Organic Search31816752.5%154.4秒
Referral1147767.5%209.0秒
AI Assistant501938.0%124.3秒

AI経由は50セッションで、全体の2.7%にすぎません。誇張する数字ではありません。ただし内訳を開くと、ChatGPTから36(平均75.4秒)、Perplexityから13(平均226.8秒)、Geminiから1(554秒)。Perplexity経由の13セッションは、平均で3分47秒読んでいます。 Directの45.9秒と比べると、来る人数と読む深さは別の指標だと分かります。

この50が明日ゼロになっても、売上も順位も変わりません。困るのはその先です。クロールを止めた瞬間、AIの回答に自分のサイトが出る余地が消えます。50は今の数字で、止めれば伸びしろごと閉じます。

同じ日のCloudflareログには、10種のAIクローラーが来ていました。YouBot 87 / PerplexityBot 48 / Bytespider 36 / ExaSearchBot 27 / Claude-User 26 / OAI-SearchBot 24 / Amazonbot 21 / ChatGPT-User 19 / ClaudeBot 18 / MoonshotBot(Kimi)14。robots.txt では既に明示的に Allow しています。ここをWAFで落とすと、robots.txt に書いた許可は意味を持ちません。 robots.txt はクローラーへのお願いで、WAFはその手前で通信を切る場所だからです。

ちなみに一番リクエストを食っていたのはAIでも検索でもなく、SEO被リンク調査ボットでした。serpstatbot 単独で724。これは落としても私には何の損もありません。

国でもUser-Agentでも切れない

シンガポールが真っ黒なら、国で切ればいい。そう考えて米国の内訳を見たら、その手は使えないと分かりました。

米国の内訳セッションエンゲージ平均滞在
desktop / Chrome1361322.9秒
mobile / Safari42435.2秒
desktop / Edge65358.3秒
desktop / Safari5188.2秒

desktop / Chrome の136セッションは、シンガポールと同じ顔をしています。ところが同じ米国の desktop / Edge は、6セッション中5がエンゲージ、平均358秒。6分近く読んでいる読者です。米国を国ごと落とすと、この6セッションが消えます。中国も124セッション中エンゲージ8(6.5%)で、シンガポールと同型でした。国という軸は、真っ黒な国には効いて、混ざっている国には効きません。

名乗りのほうも同じです。シンガポール発のUser-Agentは Android・iPhone・Mac・Windows がバラバラに並び、Chromeのバージョンは106から149まで散っていました。毎回変えているので、文字列でマッチさせる方法が成立しません。3日分を割ると、SG総リクエスト385 / 369 / 490に対して、ブラウザを名乗るものが177 / 289 / 306。GA4に載っていたのはこの列です。JavaScriptを実行してタグを発火させている以上、単純なHTTPクライアントではなくヘッドレスブラウザです。

ここまでで、使える軸が3つとも潰れました。国は混ざる、User-Agentは偽装される、IPは無数にある。残っているのは「そのリクエストがブラウザとして本物かどうかを、リクエストの中身から判定する」という軸だけです。

Cloudflareの二段構成

Cloudflare側には、この判定がフィールドとして用意されています。

一段目: cf.client.bot

Cloudflareは「Verified Bots」というリストを持っていて、Googlebot、bingbot、GPTBot、ClaudeBot、PerplexityBotなどの正規クローラが登録されています。登録には、そのボットの運営者がCloudflareに申請して、逆引きDNSやIPレンジで名乗りが本物であることを検証される手続きがあります。

cf.client.bot は「このリクエストは検証済みボットである」を返す真偽値のフィールドです。User-Agentの文字列マッチと違い、偽装したUser-Agentは true になりません。GooglebotのUser-Agentを騙るリクエストは、IPが一致しないので弾かれます。

二段目: 残りにチャレンジを出す

WAFカスタムルールは上から順に評価されるので、順序で意図を表現します。

ルール1  [skip: 後続の全カスタムルール]
  (cf.client.bot)
  or (http.request.uri.path eq "/llms.txt")
  or (http.user_agent contains "GPTBot")
  or (http.user_agent contains "ClaudeBot")
  or (http.user_agent contains "OAI-SearchBot")
  or (http.user_agent contains "PerplexityBot")
  or (http.user_agent contains "MoonshotBot")
  ...

ルール2  [managed_challenge]
  (ip.src.country eq "SG") and (not cf.client.bot)

WAFカスタムルールの二段構成。ルール1でcf.client.botとAIクローラーのUser-Agentとllms.txtをskipし、そこで抜けなかったものだけをルール2のManaged Challengeに渡す

ルール1がAIクローラーを先に逃がすので、ルール2の網には掛かりません。ルール2側にも not cf.client.bot を重ねて書いてあるのは冗長ですが、あとでルールの順序を入れ替えたときに事故らないための保険です。

ルール1にUser-Agentの文字列マッチを混ぜているのは、Verified Botsリストに載っていない新興のAIクローラーを拾うためです。MoonshotBot(Kimi)やExaSearchBotのように、実際に来ているのに検証済みリストにはまだ無い、というものがあります。文字列マッチは偽装できる穴です。ただしここでの目的は計測の汚れを取ることで、不正アクセスを止める話ではありません。穴のコストは小さいと判断しました。

/llms.txt を明示的に外しているのは、LLMO用のファイルがチャレンジに掛かったら本末転倒だからです。この漏れは実際にやりかねないので、先に書いておきます。実際に入れたルールでは /llms-full.txt /robots.txt /sitemap-index.xml も同じ扱いにしました。

入れたあとに壊れていないか確かめる

投入は本番反映なので、入れた直後に自分で叩きました。日本からの通常アクセスと、AIクローラーを名乗ったアクセスの両方です。

確認したもの結果
/ /ja/blog/200
/llms.txt /robots.txt /sitemap-index.xml200
UA = ClaudeBot / GPTBot / PerplexityBot200
UA = Qwenbot(実際には1件も来ていないが先回りで許可)200

ルール1のUser-Agentマッチが効いているかは、この叩き方では検証済みボット判定と区別がつきません。区別したいなら、Cloudflareのセキュリティイベントでどちらのルールにマッチしたかを見ます。

料金: Freeプランの5本に収まる

プランで変わるのはルールの本数だけです。

FreeProBusinessEnterprise
カスタムルール数5201001,000

この構成は2本なので、Freeの5本に収まります。実際にFreeプランのゾーンへAPIから投入して、skipmanaged_challenge の2本が通ることを確認しました。 cf.client.bot も式として受け付けられます。このサイト自身もFreeプランです。

アクションのうちプラン制限があるものには、公式に注記があります。log はEnterpriseのみ、Blockのカスタムレスポンスは Pro 以上。Managed Challengeにはその注記がありません。チャレンジの実行回数に対する従量課金も、公式ドキュメントには見当たりませんでした。ただしこれは「無料と書いてある」のではなく「課金の記載がない」なので、そこは区別しておきます。

Managed Challenge が実際にやっていること

ここが本題です。私は最初、Managed Challengeを「怪しかったらCAPTCHAを出す機能」だと思っていました。公式ドキュメントを読んだら、3箇所間違っていました。

誤解1: 「怪しくなければ何も出ない」ではない

Managed Challengeはインタースティシャルなチャレンジページを必ず返します。目的URLの手前に全画面のゲートが挟まり、そこでブラウザ環境が評価されます。

The Challenge Page intercepts the visitor from getting to the destination URL by holding the request and evaluating the browser environment for automated signals, and serving a challenge.

「何も起きずに素通り」ではなく、「ゲートは必ず通るが、大半の人間は無操作で Successful になって先へ進む」が正確です。

WAFルールで選べるアクションは3つあります。

アクション中身
Non-Interactive Challenge注入されたJavaScriptをブラウザに処理させる。操作は不要。通常5秒未満
Managed Challengeブラウザのシグナルを見て、Cloudflareがどのチャレンジを出すか動的に選ぶ
Interactive Challenge必ず操作を要求する

公式は「特別な互換性の問題がない限り、Managed Challenge以外を使うな」と明記しています。動的に選ぶぶん、人間に無駄な操作をさせる回数が減るからです。

そして、この3つのどこにもCAPTCHAはありません。信号機を数えさせる画像パズルは、いまのWAFの選択肢に載っていないのです。Interactive Challengeの説明ですら「visitorがチャレンジと対話する必要がある」としか書かれておらず、CAPTCHAという語は出てきません。

Cloudflareは2022年に「CAPTCHAをやめる」と宣言しています。当時の公表値で、チャレンジの完了にかかる時間は平均32秒から1秒へ、離脱率は従来のCAPTCHA比で31%低いとされました。CAPTCHAの利用は1年で91%減り、Managed Challengeが返す解答のうちCAPTCHAは9%まで下がっていました。

ただしこの9%という数字は2022年時点のもので、同じ記事の「年内に1%未満へ」は当時の目標であって実測ではありません。私が確認できる現在の事実は、公式のアクション一覧にCAPTCHAが存在しないことのほうです。かつてあったLegacy CAPTCHAというアクションは、いまのリファレンスには載っていません。

読者にCAPTCHAが出るのではという不安は、ここで解けます。出す設定が用意されていません。

誤解2: Private Access Token は「チャレンジを飛ばす」ではない

iOS 16以降やmacOS Ventura以降の対応環境では、OSが「これは本物の端末である」と証明するトークン(Private Access Token)をネットワーク越しに提示できます。私はこれを「PATがあればチャレンジページを見ずに済む」と理解していました。違いました。

A PAT does not automatically solve a challenge or let a visitor bypass the Challenge Page. The visitor still encounters the Challenge Page regardless of whether they have a valid PAT.

PATがあってもチャレンジページには行きます。減るのは「必要な段数」です。トークンが取得できない環境では /cdn-cgi/challenge-platform/.../pat/... へのリクエストが401を返しますが、これは正常動作で、ブロックではありません。開発者ツールで401を見つけて原因だと勘違いしないように、と公式がわざわざ書いています。

誤解3: cf_clearance は単なる有効期限つきクッキーではない

チャレンジを通過すると cf_clearance クッキーが発行されます。既定の有効期間は30分(推奨15〜45分、Challenge Passageで変更可)。ここまでは想像どおりでした。

中身が2層になっているのは知りませんでした。

  • Challenge clearance: チャレンジを解いたときに付与される。解いたチャレンジの強度に応じて3段階のレベルがある
  • Precursor clearance: セッション中の挙動に基づいて継続的に更新される

レベルには階層があります。

クリアランスのレベル何を素通りできるか
Interactive(高)Interactive / Managed / Non-Interactive の全部
Managed(中)Managed と Non-Interactive
Non-Interactive(低)Non-Interactive のみ

そして重要なのが、Challenge clearance の有効期間は設定した時間だけ持つものの、Precursor がそのセッションを怪しいと判断した時点で無効になることです。「30分間は何をしても通し放題」ではありません。クッキーは発行された端末に紐づいていて、他のマシンにコピーしても使えません。

XHRについては、時計のずれを吸収するために検証時に1時間の猶予が足されます。短い有効期間を設定したページでXHRが壊れるのを防ぐためです。

効かない場合と、踏むと危ない罠

JavaScriptを実行できる相手には効き切らない

Non-Interactive Challengeの中身は「注入されたJavaScriptをブラウザに処理させる」です。ヘッドレスブラウザはJavaScriptを実行できます。だから通過することがあります

私のサイトのシンガポール発トラフィックは、GA4のタグを発火させている以上、JavaScriptを実行しています。つまり最初から「JSを実行できる相手」です。ここは正直に見積もるべきところで、期待できるのは「消える」ではなく「減る」です。データセンターのIPレピュテーションとブラウザAPIの整合性の両方で減点されるので相当数は落ちますが、ゼロにはなりません。

AJAX / XHR は壊れる

チャレンジページはHTMLを1枚返すことでリクエストの流れを止めます。ブラウザがHTML以外のレスポンスを期待している場面――AJAXやfetch――では、この仕組みは機能しません。公式が明記しています。

This mechanism fails when the browser expects a non-HTML response, such as an AJAX or XHR (fetch) request.

APIエンドポイントやSPAを守りたい場合は、チャレンジを直接当てず、TurnstileのPre-clearanceを使います。先にHTMLページで人間確認を済ませてクッキーを発行しておき、API側はそのクッキーで通す形です。

私のサイトは静的サイトで、/rss.xml/feed.xml/atom.xml も存在しない(全部404)ので、この罠は踏みません。踏まないことを確認してから入れる、という順序が大事なところです。フィードを配信しているサイトが同じ設定を入れると、購読者のRSSリーダーが静かに壊れます。

チャレンジループ

Cloudflareの他のRules機能と併用すると、チャレンジが繰り返される状態になることがあります。公式がCautionとして挙げています。クッキーを無効にしているブラウザでも cf_clearance を保持できないので同じことが起きます。

国で切っても、混ざっている国は残る

冒頭の表に戻ります。シンガポールを落としても、米国のdesktop / Chrome 136セッション(エンゲージ13)は残ります。国という軸を選んだ時点で、この取りこぼしは構造的に決まっています。

だから対処は2本立てになります。WAFで来させないようにするのと、計測側を汚れに強い数え方にするのと。後者は具体的には、エンゲージセッション数で見るということです。冒頭の表がまさにそれで、682と6の差は国名を知らなくても一目で分かります。

WAFを入れる価値は「サーバー負荷とログの汚れが減る」ことにあって、「数字が正しくなる」ことではありません。数字を正しくするのは数え方の側の仕事です。

References

まとめ

  • GA4のセッション増2.6倍の内実は、37%がデータセンター発の自動アクセスでした。682セッション中エンゲージ6、平均滞在0.6秒
  • AIクローラーは同じ日に10種が来ていました。ClaudeBot、OAI-SearchBot、PerplexityBot、Kimi。ここを落とすとLLMOの前提が崩れます
  • 国では切れません。米国は同じ国の中に「136セッションでエンゲージ13」と「6セッションでエンゲージ5、平均358秒」が同居しています
  • User-Agentでも切れません。名乗りは毎回変わります
  • 使える軸は cf.client.bot(検証済みボットか)とチャレンジ(ブラウザとして本物か)の二段でした
  • Managed Challengeは「怪しければCAPTCHA」ではありません。ゲートは必ず通り、大半の人間は無操作で抜け、PATは段数を減らすだけで、cf_clearance はセッション挙動で継続評価されます
  • JavaScriptを実行できるヘッドレスには効き切りません。ゼロにはならず、減るだけです
  • 数字を正しくするのは数え方の側の仕事です。エンゲージセッションで数えれば、国名を知らなくても分かります
  • 費用はかかりません。カスタムルールはFreeプランで5本まで使え、この構成は2本です
  • このルールは2026-09-09にkenimoto.devへ実際に投入しました。投入直後の疎通は確認済み、SGがどれだけ減ったかは2週間後に追記します
AIくさい文章から脱出する技術 関連書籍 AIくさい文章から脱出する技術 AIくささ 消し方 · AIっぽい文章 特徴 · AI判定 誤検知 · 6モデル180サンプルを自分で測った全記録 (Kindle Unlimited 対象) 書籍ページを見る →