なぜ危ないのか
Slack Incoming Webhook URLはトークンを含む単一のURLで、このURLに向けてHTTP POSTを送るだけでチャンネルにメッセージを投稿できる。認証ヘッダは不要で、curlでもブラウザでも送信可能なため、URLを知っているだけで誰でもチャンネルを操作できる。社内の重要な通知チャンネルのWebhook URLが漏れると、混乱させるための偽情報を簡単に流せる。
DiscordのWebhook URLが漏れた場合も同様に、そのサーバーのチャンネルに誰でも投稿できる。さらにDiscordのWebhookはメッセージ内容に制限が少なく、外部画像やリンクを含む投稿も可能なため、フィッシングリンクをDiscordコミュニティのメンバーに向けて投稿するという悪用ができる。
AIが生成するHTMLにWebhook URLが含まれている場合、そのURLがGitHubのパブリックリポジトリにコミットされると、GitHub Secret Scanningが検出するまでの間にボットにURLを収集される可能性がある。GitHubはSlackやDiscordのWebhook URLパターンを検知するシークレットスキャンを提供しているが、検知まで数秒から数分のラグがあり、その間に悪用される危険がある。
- 相手がログインなしで開ける状態か確認する
- PCとスマホで最低1回ずつ表示を確認する
- 内部情報・個人情報・不要な外部送信が残っていないか見る
- レビュー期限と修正時の差し替え方を決めておく
ソースで見る場所
HTMLのソースで通知系サービスのドメインパターンをまとめて検索する。`grep -Ein 'hooks\.slack\.com|discord\.com/api/webhooks|outlook\.office\.com/webhook|api\.telegram\.org/bot' output.html`を実行し、ヒットした行にURLのフルパスが含まれていれば即座に除去が必要だ。
JavaScriptの文字列連結でWebhook URLを組み立てているケースも確認する。`const base = 'https://hooks.slack.com/services/'`のような形でURLの一部を変数に入れ、後でつなぎ合わせるコードがある場合も問題だ。結合された完全なURLは`grep`の単純な検索では見つかりにくいため、`hooks.slack.com`・`services/`・`api/webhooks`のような部分文字列でも検索する。
設定オブジェクトやJSONリテラルの中にWebhook URLが含まれていないかも確認する。`const config = { slackUrl: 'https://...' }`のような形でオブジェクトに埋め込まれていると、コードの流れを追わないと見つけにくい。`grep -n 'slackUrl\|webhookUrl\|notifyUrl' output.html`でキーの名前を手がかりに探すと見つけやすい。
- HTML内の外部script・form action・iframeを確認する
- APIキーやトークンらしき文字列がないか検索する
- 画像・CSS・JSのパスが公開後も解決できるか見る
- noindexと認証を混同せず、必要なら両方設定する
安全に共有する設定
Webhook URLをプレースホルダーに置換したHTMLを共有する場合、プレースホルダーが一目で分かる形式(`REPLACE_WITH_YOUR_SLACK_WEBHOOK_URL`)にする。受け取ったエンジニアが検索して置換するだけで動くよう、プレースホルダーは全角文字を混ぜずすべて半角英数字で記述する。
Webhook通知の動作デモをどうしても見せたい場合は、専用のテストチャンネルと専用のWebhook URLを作成し、そのチャンネルへのアクセス権がないメンバーを招待した後にURLを共有する方法が安全だ。万が一URLが流出しても、影響がテストチャンネルの投稿だけに限定される。
ギガサイト便でHTMLを共有する際は、アクセスログを確認して予想外のアクセス元がないかを監視する。Webhook URLがプレースホルダーのままであれば動作はしないが、アクセスパターンを把握しておくことで、万が一後からURLが復元されたとしても影響範囲を判断できる。
- HTML内の外部script・form action・iframeを確認する
- APIキーやトークンらしき文字列がないか検索する
- 画像・CSS・JSのパスが公開後も解決できるか見る
- noindexと認証を混同せず、必要なら両方設定する
再発防止ルール
プロジェクトのドキュメントに「Webhook URLは絶対にHTMLファイルに直書きしない。開発時はdotenvまたは環境変数、デプロイ時はCIシークレットとして管理する」という方針を明記する。AIへのプロンプトにも同じ方針を制約として追加し、生成物が常にこの方針に従う状態を作る。
GitHub ActionsのCIでSemgrepのシークレット検出(`semgrep --config=p/secrets`)を実行するstepを追加する。SlackやDiscordのWebhook URLパターンがコードに含まれていた場合にCIが落ちるようにしておくと、マージ前に必ず発見できる。
インシデント訓練として、意図的にWebhook URLをテストコードに含めてCIが検出するかを定期的に確認する。検出から通知・対応までのフローをシミュレーションしておくことで、本番でインシデントが起きたときの対応が速くなる。
- HTML内の外部script・form action・iframeを確認する
- APIキーやトークンらしき文字列がないか検索する
- 画像・CSS・JSのパスが公開後も解決できるか見る
- noindexと認証を混同せず、必要なら両方設定する
よくある質問
GitHub Secret ScanningがWebhook URLを検出した場合、Slackからどのように通知が来ますか?
GitHub Secret ScanningがSlack Webhook URLを検出すると、GitHubのセキュリティアラートとして通知される。SlackはGitHubパートナーとして登録されており、GitHubから通知を受け取ってWebhookを自動失効させる仕組みがある(push protection機能)。
AIが生成したHTMLのWebhook URLが、実際に自社のSlackに通知が届くURLかどうかをどうやって判断しますか?
Slack管理画面の「App管理」→「カスタムインテグレーション」→「Incoming Webhook」で登録済みURLの一覧を確認する。HTMLに含まれるURLがこの一覧に含まれていれば実際に機能するURLだ。一覧になければAIが架空のURLを生成している可能性が高い。
Webhook URLをHTMLから分離してサーバー側で管理する場合、どういう設計が一般的ですか?
フロントエンドからサーバーの内部API(例:`/api/notify`)にPOSTし、サーバー側がそのWebhook URLへ中継する設計が一般的だ。Webhook URLはサーバーの環境変数に置きフロントエンドには渡さない。これによりHTMLのソースにURLが現れなくなる。