本文へ移動
情シスの相棒
ニュース約18分で読めます

AIボットになりすまし.env探し 名前だけの許可は見直しを

AIボットになりすまして.envや秘密鍵を探すスキャンが続いています。UAだけで検査を飛ばすWAFの直し方と、ログの読み方、見つけたときに認証情報を替える順番が分かります。

公開 情シスの相棒編集部AIにより執筆編集部が確認:
文字
AIボットになりすまし.env探し 名前だけの許可は見直しを

この記事のまとめ

  • 穴になりやすいのはrobots.txtではなく、UAだけで検査を飛ばすWAFの除外です
  • 到達48%はCequenceの保護先の数字で、要求のほぼすべてが遮断か失敗でした
  • 攻撃元IPの遮断より先に、公開領域のファイルとログをサーバーの中で調べます
こんな方に#30代#40代#50代実務

「AIボットは通す」と決めたWAFのルール、条件はUser-Agent(UA)だけになっていませんか。そうなら、1週間以内に直しておきたいところです。Cequenceは10月28日、約32のAIボットになりすまして.envやクラウドの認証情報、SSHの秘密鍵を探すスキャンを追跡していると報告しました。

もっとも、AIボットをすべて止める話ではありません。今日いちばん先に調べるのは、公開領域に認証情報のファイルが置かれていないかです。

追跡されたのは約32の名前を使う相手

Cequenceの調査チームは、このスキャンを1つの攻撃者によるものとみています。大手クラウドから届き、OpenAI、Anthropic、Meta、Perplexityなどのクローラーやアシスタントの名前を順に使います。Webページには見向きもせず、約90のパスから、秘密の値が入っていそうなファイルだけを要求するのが特徴です。

1つのIPアドレスが約8秒で50〜190件を送ると、次へ移ります。標的を1つずつ昼夜を問わず回り、少なくとも9月下旬から10月下旬まで続きました。届いた先は金融、小売、通信、エネルギー、ソフトウェアの組織です。

偽物と分かった理由は、かなり素朴でした。同じマシンが数秒のうちにOpenAIにもAnthropicにも、MetaにもPerplexityにもなります。32の名前を均等に回す動きも、実在のボットにはありません。Claudeを名乗る要求に至っては、ClaudeBot以外を装うときもAnthropicのクローラーの連絡先が付いたままでした。Cequenceによると、文字列をコピーして貼った跡です。

名前はAIでも、AIを使った攻撃だと分かったわけではありません。NorForgeが調べた攻撃者の管理画面はAIで攻撃を進めるツールの話でしたが、今回の報告で読み取れるのは、信用されやすい名前を借りていることまでです。

48%は侵入された割合ではない

目を引く48%は、10月のある1日に、Cequenceの保護先のうちAIボットの通信があった組織の48%へ、このスキャンが届いたという数字です。分母の組織数は示されていません。そのうえ要求のほぼすべてが、遮断か失敗に終わりました。

つまり、ノックされた割合であって、ファイルを取られた割合ではないのです。

かといって、安心材料として読むのも早いでしょう。遮断と失敗の内訳は公表されておらず、ボット対策を導入している組織での結果なので、対策のないサイトで同じになるとは言えないからです。

情シスの外に関わる数字もあります。複数の組織で、AIボットと表示された通信の80%超をこのスキャンが占めていました。「AIボットのアクセスが増えた」という社内の報告も、UAで数えているなら割り引いて読む必要があります。AI検索への対応を進める広報やマーケティングの担当者には、早めに伝えておきたい話です。

3社の観測は同じ攻撃とは言い切れない

同じ型の報告は、夏にも出ていました。GreyNoiseが8月28日に、Fastlyが10月13日に、それぞれの観測を公表しています。Cequenceを加えた3社の時期を図に、数字を表にまとめました。

3社の観測の時期を示す時間の軸。GreyNoiseは7月28日〜8月23日に観測して8月28日に公表、Fastlyは9月23日の1日を観測して10月13日に公表、Cequenceは9月下旬〜10月下旬に観測して10月28日に公表。7月末から10月末まで約3か月。時期も観測の場所も違い、同じ相手かどうかも分からず、見えるのは規模より同じ手口が続いていることだとまとめている。
3社の観測と公表の時期。同じ型の報告が約3か月続くが、数字は足せない作成: 編集部 / 出典: GreyNoise、Fastly、Cequenceの報告をもとに編集部が作成
項目 GreyNoise Fastly Cequence
観測した期間 7月28日〜8月23日 9月23日の1日 9月下旬〜10月下旬
観測した場所 自社の観測網 Next-Gen WAFとBot Managementの利用者 自社の保護先
使われた名前 13(うち6つが同じ824アドレスから) 主な攻撃で12 約32
IPアドレス 824(795の/24に分散) 約10,200(うち約1,600で94%) 1日に300超、1週間で400弱
探したパス 上位10件を公開 96のファイル名 約90
要求の規模 同じフィンガープリントの通信で数百万件 1日に約630万件、主な攻撃で10万超のサイト 1アドレスで約8秒に50〜190件
ファイルが取られたか 分からないと明記 記述なし ほぼすべて遮断か失敗

3社に共通するのは、AIの名前をいくつも借りて、.envなどの認証情報のファイルを探す動きです。一方で、名前の数もIPの数もそろいません。3社ともボット対策や脅威情報を提供する会社で、数字はそれぞれの顧客や観測網の中のものです。

同じ相手かどうかはどの報告からも分からず、観測の日数も違うので、数字は足さないほうが安全です。ファイルを取られた会社の例も、3社とも挙げていません。

言えるのは規模よりも、手口の定着でしょう。7月末から10月末までの約3か月に、別々の観測網で同じ型の動きが報告されました。

狙う先はかなり絞られています。Fastlyの1日の集計では、.env系が約400万件で、全体の約630万件の6割強を占めました(割合は編集部で計算)。

Fastlyの横棒グラフ。上から/.env 149,329、/.env.local 90,991、/@fs/.env 90,451、/api/.env 76,162、/.env.development 71,146、/admin/.env 69,064、/.env.production 68,914、/../../../../.env 60,844、/.git/HEAD 60,547、/.git/config 60,502、/secrets.json 56,645、/@fs/app/.env 53,631。
Fastlyが公開した、偽のAIボットが1日に狙ったパスの上位。/.envが最多出典: Fastly「AI Bot Cosplay」(引用)

上位に入った/@fs/.envは、Fastlyによると、Viteの開発サーバーのファイル用ルートを狙うパスです。Vitestの開発サーバーから.envが読める脆弱性もそうでしたが、開発用のサーバーは本番の点検から漏れやすいところ。検証環境や開発サーバーが外から届くなら、そちらも同じように調べます。Viteを使っているなら、GreyNoiseは修正版への更新も勧めています。1

robots.txtの許可は穴にならない

AI検索に載せたいとき、設定する場所は2つあります。混同すると、直す場所を間違えます。

1つ目のrobots.txtは、クローラーへのお願いにすぎません。規格のRFC 9309も、アクセスの認可ではないと明記しています。

These rules are not a form of access authorization.

出典: RFC 9309 Robots Exclusion Protocol

GreyNoiseの観測でも、偽物の6つの名前はrobots.txtを一度も読みませんでした。Allowを書いても穴は増えないわけです。逆にDisallow: /.envと書くと、RFC 9309が注意するとおり、ファイルの場所を自分で公開することになります。

2つ目が、WAFやCDNの除外、レート制限の例外です。こちらは通信の扱いが本当に変わります。Fastlyは、攻撃者がAIボットの名前を借りる理由として、許可リストに入れたりレート制限を緩めたりするサイトがあることを挙げました。Cequenceも、どのエージェントが信頼されているかを試す動きを見ていると書きました。32の名前を均等に回すのは、どの名前なら通るかを総当たりで試しているようにも見えます。

影響が大きいのは、検査ごと飛ばす設定でしょう。Perplexityの説明にあるCloudflareの設定例では、条件に合う要求についてマネージドルールやレート制限をスキップできます。同社の例はUAとIPの両方が条件です。これがUAだけなら、UAを書き換えた要求は、中身を問わず検査なしで届きます。.env探しに限らず、SQLインジェクションでも同じです。

除外の条件をUAだけ、UAとIP、UAとIPにパス遮断を加えた形の3列で比べた表。本物のクローラーはどれも除外で通る。偽のClaudeBotが/.envを要求した場合と偽のChatGPT-Userが/.git/configを要求した場合は、UAだけなら検査なし、UAとIPなら通常のルール、パス遮断を加えると遮断。偽のUAのSQLインジェクションは、UAだけなら検査なし、ほかは通常のルール。名前を出さないスキャナーは、パス遮断を加えた形で遮断。下に、公開領域にファイルがあれば中身を返す、どのパスにもトップを返すサイトは200でも中身はトップページ、ファイルがなければ404と示す。
除外の条件ごとに、届く要求の扱いがどう変わるかをまとめた表作成: 編集部 / 出典: GreyNoise、Fastly、Cequenceの報告とPerplexityの説明をもとに編集部が作成

広報やマーケティングの依頼で急いで足した例外は、見直しから漏れやすいと編集部は考えます。セキュリティの確認より、検索に載ることを目的に足した設定だからです。CloudflareとOpenAIの検索の試験の記事でも触れたとおり、検索で見つけてもらうことと、通信を許可することは別の話です。

本物かどうかは接続元で決める

UAは自己申告で、決め手になるのは接続元のIPアドレスです。GreyNoiseが824のアドレスを各社の公開の一覧とすべて照らしたところ、1件も一致しませんでした。主な会社の公開の状況を、10月31日に確認しました。

会社 主な名前 IPの一覧(リンク先) 範囲の数 robots.txtの扱い
OpenAI GPTBot、OAI-SearchBot、ChatGPT-User gptbot.json、searchbot.json、chatgpt-user.json 18、39、235 ChatGPT-Userは適用されない場合がある
Anthropic ClaudeBot、Claude-User、Claude-SearchBot bots.json 14 従うと明記
Perplexity PerplexityBot、Perplexity-User perplexitybot.json、perplexity-user.json 13、10 Perplexity-Userは原則として従わない
Google Googlebotなど 種類ごとのJSONと逆引き 未集計 利用者の操作による取得は従わない
Meta meta-externalagentなど 説明のページに記載なし なし meta-externalfetcherは従わない場合がある

まず、一覧は動きます。ファイルの作成日時を見ると、ChatGPT-Userは10月27日、Perplexityの2つは10月28日、Anthropicは10月20日でした。OAI-SearchBotのように1月2日のままのものもあり、日時が新しくても中身が変わったとは限りません。それでも、WAFに手で貼ったままだと、いずれ本物を止めかねないでしょう。Perplexityの説明にも、定期的に取り直す自動化を組むようにとあります。

「robots.txtを読まないから偽物」と決めつけるのも危険です。GreyNoiseは手がかりに挙げていますが、表のとおり、利用者の操作で動くボットはrobots.txtに従うとは限りません。使えるのはクローラーの判定までと考えたほうがよいでしょう。

一方、Google-Extendedは迷わずに済みます。Googleの説明ではHTTPのUAとして送られない名前なので、ログにあれば偽物です。GreyNoiseの観測では263,849セッションがこの名前を使っていました。

Metaは少し事情が違います。説明のページはFacebookExternalHitについて、許可リストにUAかIPを入れるよう案内し、IPのほうが安全だと注記するものの、同じページにIPの一覧は載っていません。

照らすIPにも落とし穴があります。X-Forwarded-Forから取ったアドレスで判定すると、送り手が書き換えられてしまいます。RemoteAddrとSocketAddrの違いとして、Azure Front DoorのWAFの記事で取り上げたのと同じ話です。名前をIPに変えても、そのIPが自己申告なら穴は残ります。

一覧を自分で持たない方法もあります。Cloudflareのcf.client.bot、AWS WAF Bot Controlのbot:verified、FastlyのVERIFIED-BOTは、製品側で確認した結果を条件に使えます。どのボットが対象かは製品ごとに違うので、通したい名前が入っているかを先に見ておきます。署名で確かめるWeb Bot Authも、Cloudflareは確認方法に入れていますが、土台はまだIETFのドラフトです。

影響するのはUAで通している会社

急ぐかどうかは、AIボットを許可しているかではなく、どこで何を条件に許可しているかで分かれます。

自社の状態 何が起きるか 動く時期
公開領域に.envや秘密鍵がある 名前に関係なく、届いた要求に返してしまう 今日
WAFやCDNの除外、レート制限の例外がUAだけ UAを書き換えた要求が検査なしで届く 1週間以内
AIボットの件数をUAで集計している 報告の数字が膨らむ 次の報告まで
robots.txtで許可しているだけ 影響しない。穴は増えない ログの確認だけ
UAとIP、または確認済みボットの判定で許可 名前を偽っても除外に当たらない 一覧の更新を確認
自社でWebサーバーを持たない 直接は影響しない 制作会社に手順1を依頼

見直しの本命は2行目です。AI検索を意識して設定を変えた会社は、ここに入りやすいはずです。

今日のうちに確認する手順

コマンドはnginxとcombined形式のログの例で、Apacheならパスを読み替えます。外から自社の/.envを開いて試すのではなく、すべてサーバーの中で調べてください。

  1. 公開領域にファイルがないかを検索します。ドキュメントルートはWebサーバーの設定で確認し、検証環境と開発サーバーも含めます。出るのはファイル名と更新日だけで、中身は開きません。
nginx -T 2>/dev/null | grep -E '^\s*(root|alias)\s'
find /var/www/example/public \( -name '.env' -o -name '.env.*' -o -name '.git' -o -name '.git-credentials' \
  -o -name '.aws' -o -name '.ssh' -o -name 'id_rsa*' -o -name 'id_ed25519*' -o -name 'id_ecdsa*' \
  -o -name '*.pem' -o -name '*.key' -o -name 'secrets.json' -o -name 'credentials' \
  -o -name '*.sql' -o -name '*.sql.gz' \) -not -path '*/node_modules/*' \
  -printf '%TY-%Tm-%Td %p\n' | sort
  1. ログから秘密のパスへの要求を抜き出します。約32の名前は全部が公開されているわけではないので、まずUAを絞らずに数を見ます。
zgrep -hE '"[A-Z]+ [^"]*(\.env|\.git/|\.aws/|\.ssh/|id_rsa|id_ed25519|id_ecdsa|secrets\.json)' \
  /var/log/nginx/access.log* > secret-paths.log
awk -F'"' '{print $6}' secret-paths.log | sort | uniq -c | sort -rn | head -30
  1. 応答を見ます。curlなど名前を出さない要求や、一覧にない名前の要求で取られた場合を見落とさないよう、まず名前で絞らずに200と206を確認し、そのあとでAIボットの名前に絞ります。200があっても、すぐに漏えいとは限りません。どのパスにもトップページを返すサイトでは、/.envにも200が並ぶからです。Cequenceによると、スキャナーも無意味なパスを先に送り、そうしたサイトを見分けています。サイズがトップページと同じならその可能性が高く、違うサイズなら手順1の結果と突き合わせます。
awk '$9 ~ /^(200|206)$/ {print $1, $7, $9, $10}' secret-paths.log | sort -u
grep -iE 'GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-User|Claude-SearchBot|PerplexityBot|Perplexity-User|meta-external|meta-webindexer|facebookexternalhit|Google-Extended|Amazonbot|CCBot|DuckDuck|DuckAssist' \
  secret-paths.log > ai-named.log
awk '{print $9, $10}' ai-named.log | sort | uniq -c | sort -rn
  1. 公開の一覧と照らします。上の表のJSONを同じフォルダーに保存し、次のスクリプトをcheck_ai_bots.pyとして保存し、python3 check_ai_bots.py ai-named.log | sort | uniq -c | sort -rnで実行します。CDNやロードバランサーの内側では先頭が中継のIPになることがあるので、接続元のIPが先頭に来るログで動かしてください。秘密のパスに来た「範囲外」は、偽物とみてかまいません。「範囲内」なら、自社のページからリンクしていないかを見ます。Metaなど、スクリプトの対象にしていない名前は「一覧なし」と出ます。
import ipaddress, json, sys
LISTS = {
    "GPTBot": "gptbot.json", "OAI-SearchBot": "searchbot.json", "ChatGPT-User": "chatgpt-user.json",
    "ClaudeBot": "bots.json", "Claude-User": "bots.json", "Claude-SearchBot": "bots.json",
    "PerplexityBot": "perplexitybot.json", "Perplexity-User": "perplexity-user.json",
}
nets = {}
for name, path in LISTS.items():
    prefixes = json.load(open(path))["prefixes"]
    nets[name] = [ipaddress.ip_network(p.get("ipv4Prefix") or p["ipv6Prefix"]) for p in prefixes]
for line in open(sys.argv[1], errors="ignore"):
    ip = ipaddress.ip_address(line.split()[0])
    ua = line.rsplit('"', 2)[-2].lower()
    if "google-extended" in ua:
        print("偽物", ip, "Google-Extended")
        continue
    for name, networks in nets.items():
        if name.lower() in ua:
            print("範囲内" if any(ip in n for n in networks) else "範囲外", ip, name)
            break
    else:
        print("一覧なし", ip, ua[:60])
  1. 許可の条件を直し、秘密のパスを止めます。Webサーバーの設定とWAFのルールから、UAを条件にしている所を探します。Cloudflareの式なら、Perplexityの例のようにUAとIPの両方を条件にします。
grep -RniE 'gptbot|claudebot|chatgpt-user|perplexity|http_user_agent|SetEnvIf.*User-Agent' /etc/nginx /etc/apache2 2>/dev/null
(http.user_agent contains "PerplexityBot" and ip.src in {<PerplexityBotのIP範囲>})

点で始まるパスは、名前を見ずに止めて構いません。.well-knownは証明書の更新などに使うので除きます。nginxは正規表現のlocationを書いた順に評価するので、ほかの正規表現より前に置きます。

location ~ /\.(?!well-known/) { return 404; }

この1行で止まるのは、点で始まるパスだけです。/secrets.jsonや/backup/id_ed25519、.sqlのファイルは止まらないので、手順1で見つけたら公開領域から外します。^~を付けたlocationに合うパスも、正規表現より優先されて素通りします。反映の前にnginx -tで構文を確認してください。

GreyNoiseが勧めるとおり、/.envや/.aws/credentials、/.git/configへの要求には通知も付けておきます。攻撃元IPの遮断リストに時間をかける必要はありません。Cequenceの観測では、9月下旬のアドレスは1か月後に1つも現れませんでした。

見つかったら外から使える認証情報を先に替える

GreyNoiseの助言は明快です。Webのパスから届いたことのあるクラウドのキーは替える。読めたものは読まれたと考える。ログに200が残っていなくても、無事の証明にはなりません。保存期間より前のことは分からないからです。

替える順番は報告にないので、編集部で組みました。基準は、盗まれたらそれだけですぐ使えるかどうかです。

  1. Web、WAF、CDNのログを別の場所へ控えます。ローテーションで消える前に済ませます。
  2. ファイルを公開領域の外へ移します。すぐに移せないなら、先にWebサーバーの設定でそのパスを404で止めます。次のコマンドで変数名だけを控えれば、値を画面に出さずに替える対象を洗い出せます。小文字の変数名は出ないので、ある場合は別に確認します。.gitが見つかったときは、履歴に残った古い値も対象です。
grep -oE '^[[:space:]]*(export[[:space:]]+)?[A-Z_][A-Z0-9_]*=' .env | sed 's/=$//'
  1. クラウドのキーとSSHの秘密鍵を替えます。AWSの手順は、2本目のキーを作ってアプリを更新し、古いキーを無効化して、時間をおいて削除する順です。ただ、読まれた形跡があるなら、アプリが止まるのを覚悟して無効化を先にします。SSHは、対応する公開鍵を踏み台を含む全サーバーのauthorized_keysから外します。
  2. データベースのパスワードと、決済やメール配信、AIなどのAPIキーを替えます。
  3. セッションの署名に使う秘密の値は最後に替えます。サービスによっては利用者が一斉にログアウトするので、案内を用意してから進めます。
  4. 使われた形跡を調べます。AWSのlookup-eventsで引けるのは、今のリージョンの管理イベントを過去90日分までです。そのためリージョンを順に回します。次のコマンドはAWSの説明をもとに組んだもので、実行での確認はしていません。顧客データのあるデータベースに届く認証情報なら、法務にも早めに声をかけます。
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  aws cloudtrail lookup-events --region "$r" --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=<古いキーのID>
done

AI検索に載せたいという相談は、これからも増えるでしょう。そのとき情シスから返す質問は、どのボットを通すかのほかにもう1つあります。「そのボットが本物だと、何で判断しますか」。

注

  1. GreyNoiseが挙げるViteの更新先は、6.2.3、6.1.2、6.0.12、5.4.15、4.5.10です。 ↩

よくある質問

QAIボットをすべて遮断すれば解決しますか。

A解決しません。偽物は名前を替えるだけで抜けられますし、本物まで止めればAI検索に載らなくなる影響も残ります。秘密のパスへの要求を、名前に関係なく止めるほうが確実です。

Q公開のIP一覧がないボットは、どう扱えばよいですか。

AIPでは本物か判断できないので、許可の条件に入れず、通常のルールで判定するのが無難です。使っているWAFの確認済みボットの機能が対応していれば、その判定を使えます。

Q外から自社の/.envを開いて試してもよいですか。

A本番では避けたほうが無難です。自社のWAFや監視が攻撃として扱い、担当者のIPが止まったり、調査中のログに紛れたりします。ファイルの有無はサーバーの中で調べます。

出典・参考

編集部が確認:。確認した監修者は、この下に載せています。

執筆

情シスの相棒編集部AIにより執筆

書いた AI: AI記者(解説)

法令・官公庁の発表・企業の公式発表などの一次情報を調べて、情シス向けの解説・比較・テンプレの記事を書きます。数字と日付は出典と照らして確かめ、記事の最後に出典を載せています。使っているAI: Anthropic Claude。

監修

佐々木 航株式会社Game

代表取締役。工学部出身で、21歳で起業してからシステム会社の経営一筋で8期目。アプリの受託開発から始め、いまはAIを使ったシステムソリューションを手がけています。150社以上の企業とのプロジェクトで培った開発と情報システムの現場の経験と声を形にし、激動の時代を乗り越えたいと考えています。

メモ: 掲載時に自動で(しばらくの間)

記事は AI が一次情報を調べて書き、機械の検査を通してから公開しています。人が見ているもの・見ていないものはAI の使い方に書いています。まちがいに気づいたら訂正・ご指摘からお知らせください。直したら、記事の最後に何を直したかを書きます。

役に立ったら、いいねと共有をお願いします