1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WAFで偽装AI UAを弾いても解析からは消えない — 403も集計され、スキャンは本物と同じ名前に移る

1
Posted at

AI クローラーを騙った認証情報スキャン(/.env/wp-config.php を叩くやつ)を、Cloudflare の WAF で遮断しました。入口で止まれば解析の数字も綺麗になる、と思っていました。

1か月後の数字はそうなりませんでした。遮断していた Perplexity-User は8日間で595リクエスト計上されたまま、内訳は403のみ。さらにスキャンは、遮断していない AmazonbotChatGPT-User という本物のクローラーが使っているのと同じ名前に移っていました。

WAF で偽装 UA を弾くときに先に知っておきたかったことを、実測つきでまとめます。

遮断したリクエストは解析に残る

Cloudflare の GraphQL Analytics API(httpRequestsAdaptiveGroups)は、エッジが返した応答を集計します。WAF でブロックしたリクエストは消えず、403 として計上されます。

Perplexity-User を名乗るリクエストの推移がそのまま証拠になりました。ルール投入前は404(オリジンに到達して存在しないパス)、投入後は403(エッジで遮断)。ステータスが変わっただけで、件数は増えています。

スナップショット(8日窓) 403 404
08-03(ルール投入当日) 1 4
08-08 2 4
08-16 297 0
08-22 311 0
08-28 595 0

遮断は効いています。404 が消えたので、リクエストはオリジンに届いていません。それでも「AI クローラーからのアクセス数」を UA 別に数える限り、この595件は数字の中に残ります。

つまり WAF はオリジンを守る道具で、集計を直す道具ではありません。 計測の汚染を落としたいなら、取得側(集計スクリプト側)で分類する必要があります。

入れたルール2本

対象は Cloudflare Pages 上の静的サイト、無料プランのゾーンです。ルールは API から冪等に投入していて、description を目印に、あれば更新・無ければ作成します。

1本目はパスで弾きます。UA は見ません。

SCAN_PATH_TOKENS = [
    "/.env", "/.git", "/.aws", "/.svn", "/.ssh",
    "/wp-", "wp-admin", "wp-login", "wp-includes", "wp-config",
    "wlwmanifest", "xmlrpc.php",
    "secrets.json", "service_account", "/actuator/", "/api/auth/session",
    "phpinfo", "/.npmrc", "/.htpasswd", "id_rsa", "config.json",
]

scan_expr = " or ".join(
    f'(http.request.uri.path contains "{t}")' for t in SCAN_PATH_TOKENS
)

2本目は UA で弾きます。not cf.client.bot(Cloudflare が検証できていない)を条件に付けるのが要点です。

SPOOFED_ONLY_UAS = [
    "Bytespider", "cohere-ai", "anthropic-ai", "Perplexity-User", "Google-Extended",
]

ua_expr = "(not cf.client.bot) and (" + " or ".join(
    f'http.user_agent contains "{u}"' for u in SPOOFED_ONLY_UAS
) + ")"

投入は Rulesets API に PUT するだけです。

API_BASE = "https://api.cloudflare.com/client/v4"
PHASE = "http_request_firewall_custom"

# 既存ルールを取得 → description が一致するものを差し替え → 全体を PUT
entrypoint = api(token, "GET", f"/zones/{zone_id}/rulesets/phases/{PHASE}/entrypoint")
rules = entrypoint["result"]["rules"]
# ... TAG(description の接頭辞)で一致するものを新しい定義に置き換える ...
api(token, "PUT", f"/zones/{zone_id}/rulesets/{ruleset_id}", {"rules": rules})

トークンは Zone/WAF/Edit + Zone/Zone/Read の2権限で足ります。解析取得用のトークン(Analytics 読み取りのみ)では403になるので、別に発行します。

ハマりどころ1: starts_with はルート直下しか一致しない

ゾーンには元から手動で作ったルールがありました。

starts_with(http.request.uri.path, "/.env")

これだと /.env は止まりますが、実際に来ていた /server/.env(18回)・/admin/.env/app/.git/HEAD は全部すり抜けます。ルールがあるのにスキャンが続いている状態でした。contains に変えると深い階層も止まります。

http.request.uri.path は正規化後のパスで判定されるので、%2e のようなエンコード回避も効きません。

ハマりどころ2: ブラックリストは後追いになる

パスのトークン一覧は、書いた分しか止まりません。08-28 の集計には、AI 名乗りのリクエストの404が300件残っていました。叩かれていたのは以下のようなパスで、どれも一覧に入っていません。

  • /secrets.ymlsecrets.json は書いたが .yml は書いていない
  • /id_ecdsaid_rsa は書いたが id_ecdsa は書いていない
  • /.zshrc
  • /@fs/proc/self/environ

config.json も最初は eq "/config.json" の完全一致で書いていて、/runtime-config.json/api/runtime-config.json に抜けられました。トークンを足すときは、サイトマップの実在パスに同じ文字列を含むページが無いかを確認してから広げるのがおすすめです(広げすぎると正当な配信物を巻き込みます)。

弾いた名前は捨てられ、本物と同じ名前に移った

ここがいちばん効いた発見でした。スキャン判定になったリクエストを名乗り別に並べると、こうなります。

名乗っている UA 08-08 08-16 08-22 08-28
Perplexity-User(ルール2で遮断) 4 147 173 387
Amazonbot 0 67 284 119
ChatGPT-User 21 51 181 67
OAI-SearchBot 9 19 74 25
GPTBot 16 18 72 26
ClaudeBot 13 16 70 27

遮断した Perplexity-User はその後も使われ続け、そこに弾いていない名前が上乗せされました。叩くパスの種類が増えたのではなく、同じ種類のスキャンが複数の名乗りに広がっただけです。

やっかいなのは、AmazonbotChatGPT-User は本物も同じ文字列で来ることです。同じ8日間の検証結果はこうでした。

名乗り 検証済み 未検証
Amazonbot 135 489
ChatGPT-User 211 336

UA 一致で弾けば、記事を216件取りに来た本物の ChatGPT-User も一緒に落ちます。UA だけを条件にしたブロックリストは、偽装が「本物と共有する名前」に移った時点で使えなくなります。

逆に、実在しない名前は恒久的に弾けます。Google-Extended は HTTP リクエスト用の User-Agent を持たないと Google のクローラー一覧に明記されていて(robots.txt の制御用トークンです)、名乗った時点で全て偽装だからです。anthropic-aicohere-ai も同じ扱いにしています。

まとめ

  • WAF でブロックしても、解析には403として残る。遮断=計測の修正ではない
  • 実在しない UA(Google-Extended など)は UA 一致で弾いてよい
  • 本物と共有する UA(ChatGPT-User / Amazonbot)は UA で弾けない。cf.client.botverifiedBotCategory検証結果で判断する側に寄せる
  • パスのブラックリストは contains で書き、サイトマップと突き合わせて広げる

「対策したのにスキャンが減らない → 減った」の推移をどう評価するか(ルール無変更の週に急減し、ゾーン全体の403は増えていた話)、遮断と計測を別レイヤーに分けた運用の形は Aulvem 本家にまとめました → Aulvem|偽装UAをWAFで弾いた1か月 — 遮断した403も解析に残り、スキャンは本物と同じ名前に移った

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?