「bot」を、一括りにしていた
うちのログモジュールは、bot だと判断したら bot=1 を立てます。それだけでした。
集計すると1日あたり2,000件を超えます。数として眺めているぶんには、それで足りていました。
ところが中身を見ると、性質がまるで違うものが混ざっています。
Googlebot と、/wp-login.php や /.env を順に叩く
python-requests が、同じ箱に入っている。
片方は絶対に弾いてはいけない相手で、もう片方は弾きたい相手です。 同じ数字で数えていては、どちらの判断もできません。
役割で、3つに分ける
| 分類 | 性質 | 扱い | 実測 |
|---|---|---|---|
| 正規クローラ | 検索エンジン・SNSのプレビュー取得 | 絶対に弾かない | 212 |
| 攻撃 | 脆弱性探索・偽装スキャナ | 遮断候補 | 1,206 |
| その他(グレー) | SEOツール・正体不明 | 判断が要る | 978 |
分けてみると、攻撃が全体の55%を占めていました。そして 正体不明のグレーが45%近くある。この3つ目がいちばん扱いに困る、というのが後で分かります。
弾いたら、終わり
Googlebot を遮断すれば、いずれ検索結果から消えます。 アクセス解析の数字が下がってから気づく類の事故で、取り返しがつきません。
だから分類器の設計は、「攻撃を見つける」ではなく「正規を守る」から始めるべきでした。 最初に正規クローラのリストを置き、そこで確定させ、以降のルールに触れさせない。 この順序が、後で効いてきます。
Twitterbot や Discordbot、LINE のリンクプレビュー取得なども、弾けば「共有したときに画像が出ない」という形で影響が出ます。検索エンジンほど致命的ではありませんが、気づきにくい分だけ厄介です。
実装の粗さが、痕跡になる
攻撃側は、いくつかの形で自分を明かします。いちばん確かなのは何を叩いたかです。
/wp-login.php、/.env、/actuator/env——
UAを見るまでもなく、探索の意図がパスに出ます。
面白いのは、そこから先です。ツールの実装の粗さが、UAに残っていることがあります。
綴りを間違えている
Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 ^^^^^^^ ^^^^^ Mozilla → Mozlila Build → Bulid
2箇所とも綴りが違います。 本物のブラウザがこの文字列を送ることはありません。UAをコピペで作った痕跡です。
UAが途中で切れている
# 記録されたもの Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML # 本物の Safari はこう続く ... AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1 ↑ 最初のカンマで切れている
本物のUAを最初のカンマで切ると、記録された文字列と完全に一致しました。 UAのリストをカンマ区切りで読み込み、引用符の処理を忘れたまま分割した—— そういうツールの痕跡です。
いちばん難しいのは、3つ目
SEO分析ツール、AI学習用のクローラ、正体不明のスクレイパー。 害があるとも無いとも言い切れない相手が、実測で978件ありました。
判断の材料になるのは、このあたりです。
| 見るところ | 意味 |
|---|---|
robots.txt を読むか | 作法を守る意思があるか |
| 頻度は妥当か | サーバーへの負荷を考えているか |
| UAに連絡先URLがあるか | 身元を明かす気があるか |
| 攻撃パスを叩くか | 探索の意図があるか |
攻撃判定を先にすると、Googlebotが消える
「ブラウザを名乗るbotは怪しい」——これは妥当なルールに見えます。
実際、偽装スキャナの多くは Mozilla/5.0 (Macintosh... のような
ブラウザそっくりのUAを使います。
ところが、このルールを先に評価すると事故が起きます。
# Googlebot スマートフォン版 Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) # Applebot Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot)
どちらもブラウザとして始まります。 「Linuxを名乗る」「Macintoshを名乗る」というパターンに、 正規クローラが真っ先に引っかかります。
だから順序はこうでなければなりません。正規の判定を最初に置き、そこで確定させる。 該当したら、以降のルールには一切触れさせない。デモで両方を試せます。
パターンは、文脈なしでは使えない
ここは実装していて肝を冷やした部分です。 検証のために本物の Chrome を分類器に通してみました。
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...
→ 判定:攻撃(Windowsを名乗る)
当然です。ブラウザなのだから、ブラウザを名乗ります。 このパターンを全トラフィックに当てたら、訪問者全員を遮断します。
実装では事故が起きません。この分類を bot=1 と判定済みのレコードにしか適用していないからです。
裏を返せば——「ブラウザを名乗るのが怪しい」のは、
すでに何らかの理由でbotと判定された集合の中でのことでした。
UAは、誰でも名乗れる
ここまでの設計には、大きな穴がありました。 正規クローラの判定を、UA文字列だけで行っていたことです。
UAは自己申告です。検証されていません。
Googlebot と書いて送れば、こちらのホワイトリストを通過して除外されます。
「正規を守る」ために置いた最優先のルールが、最も緩い判定になっていました。
逆引き → 正引きの往復で確かめる
Google も Bing も、公式に検証方法を案内しています。2段階です。
# ① IPから名前を引く(PTRレコード) $ dig -x 66.249.66.1 +short crawl-66-249-66-1.googlebot.com # ② その名前を正引きして、元のIPに戻るか $ dig crawl-66-249-66-1.googlebot.com +short 66.249.66.1 ← 戻った。本物
②が肝です。 PTRレコードはIPの持ち主が自由に書けます。
攻撃者が自分のIPのPTRに crawl-1-2-3-4.googlebot.com と書くことは可能。
しかし googlebot.com のゾーンは Google しか触れないので、
正引きで元のIPに戻すことはできません。
実装したら、実際に見つかった
cron で検証してMongoDBに貯める仕組みを入れました。数日で結果が出ました。
| 逆引き結果 | 名乗り | 正引き |
|---|---|---|
| …ips.acedatacenter.com | Googlebot | 不可 |
| …ips.acedatacenter.com | Googlebot | 不可 |
| …ips.acedatacenter.com | Googlebot | 不可 |
| …ips.acedatacenter.com | Googlebot | 不可 |
| …ips.acedatacenter.com | Googlebot の一語だけ | 不可 |
| …host.colocrossing.com | Googlebot | 不可 |
7日間で6件。 すべてホスティング事業者のIPでした。 しかも全件とも正引きすらできません—— PTRに名前を書いただけで、対応するAレコードを用意していない。身元を繕う努力すらしていません。
注目したのは分布です。1つの /24 に3件集中していました。
個別のIPを弾くより、サブネット単位で止めたほうが効率が良い。
そして5件目の名乗りが Googlebot の一語だけというのも印象的でした。
UA文字列を整える手間すらかけていない。それでも、従来の実装なら通過していたのです。
今度は、こちらが疑いすぎた
検証を入れて満足していたら、詐称リストに身に覚えのないものが混ざりました。
IP : 147.92.179.117 PTR : poker-117.line-apps.com ← 正引きも一致する UA : facebookexternalhit/1.1;line-poker/1.0
LINEのリンクプレビュー取得でした。
line-apps.com は正当なドメインで、往復確認も通ります。
facebookexternalhit 互換のUAを使うのは、
OGタグを返してもらうための一般的な作法です。
「検証できない」と「偽物」は違う
原因は私の設計ミスでした。ホワイトリストに14種類のクローラを入れたのに、 逆引きで検証できるドメインは一部しか登録していなかったのです。
| 事業者 | 検証方法 | 逆引きで判定 |
|---|---|---|
| Google / Bing / Apple / Yandex / Baidu | 公式に逆引きを案内 | できる |
| Slackbot / Discordbot / LINE など | IP範囲の公開など、別の方式 | できない |
登録していないドメインは、一致するはずがありません。 それを「詐称」と呼んでいた。正しくは「こちらに検証手段が無い」だけです。 相手の問題ではなく、こちらの限界でした。
分類器を、動かしてみる
実際に観測したUAを含む9種類を、分類器に通します。 評価順序と適用範囲を切り替えて、何が起きるか見てください。
「攻撃パターンを先に」を選ぶと、Googlebotスマホ版と Applebot が攻撃判定になります。 さらに偽Googlebot は素通りします——UAが本物と同一なので、 攻撃パターンには引っかからず、次の正規判定で除外されてしまう。 攻撃を先に見ても、詐称は防げません。
適用範囲を「全トラフィック」にすると、本物のChromeまで攻撃判定になります。 パターンは、それが成り立つ集合の上でしか意味を持ちません。
守るものから、設計する
botを分類する仕事は、「攻撃を見つける」問題ではありませんでした。 弾いてはいけないものが混ざっている以上、まず正規を守り、 そのうえで攻撃を見つける——順序が逆だと、守るべきものから先に壊れます。
そして、守るための判定こそ厳しく検証すべきでした。 「Googlebot と名乗れば除外」というルールは、攻撃者がいちばん簡単に悪用できる穴です。 最優先で置いたルールが、最も緩かった。
この記事のまとめ
- botは役割で3つに分ける。正規・攻撃・グレー。同じ数字で数えない。
- 設計は「攻撃を見つける」ではなく「正規を守る」から始める。
- 評価順序が命。攻撃判定を先にすると、Googlebotスマホ版とApplebotが引っかかる。
- 判定条件は適用範囲とセット。全トラフィックに当てれば、本物のChromeも攻撃になる。
- UAは自己申告。逆引き→正引きの往復で確かめる。PTRは相手が書けるが、ゾーンは書けない。
- 実装したら7日で6件の偽Googlebot。全件ホスティング事業者、正引きすら不可。
- 「検証できない」と「偽物」は違う。判定は3値にして、わからないものは保留する。
- 実装の粗さ(綴り誤り、切れたUA)は隠せない。相手のミスが手がかりになる。