1. はじめに
ソーイ株式会社の西浦です。
本稿は、セキュリティ診断の指摘からCSP導入を進める取り組みの第2回です。
まず、CSPの基本的な仕組みや、Sentryを用いたレポート収集基盤の構築については、以下の前編記事をご参照ください。
前回の記事:
[セキュリティ診断でCSP不備を指摘されたので、まずはSentryで違反レポートを集める仕組みを構築した話]
前編で構築した「Report-Onlyモード」を本番環境で運用した結果、開発環境では確認されなかった「Google関連サービスの通信ブロック」という課題が顕在化しました。本稿ではその解決策を詳報します。
2. 【おさらい】CSP(Content-Security-Policy)の基本
実装に入る前に、CSPの役割を簡単に整理します。詳細については[前編]をご参照ください。
- 目的: ブラウザ側で「信頼できるソース」のみを実行・読み込み許可し、XSS等の攻撃を無効化する。
- 制御方法: HTTPレスポンスヘッダ(またはmetaタグ)でポリシーを定義。
-
主な要素:
-
script-src:スクリプトの実行許可(Nonce等を利用)。 -
connect-src:API通信やWebSocketの送信先を制限。 -
frame-src:iframeで埋め込む外部サイトを制限。
-
3. 開発と本番で生じた「検知状況」の乖離
開発環境での検証フェーズでは、主要なGoogleドメイン(google.com, google.co.jp)の許可のみで正常な動作を確認できていました。しかし、本番環境で多様なユーザーアクセスが発生した結果、開発時には一度も検知されなかった海外のGoogleドメイン(google.fr, google.it, google.caなど)がポリシー違反として報告されました。
運用開始からわずか3日間で、計6カ国から20件以上の違反レポートが検知されました。 数としては少なく感じるかもしれませんが、日々異なる国からの違反が報告される状況は、手動で一つずつドメインを許可していく対応では限界があることを示唆していました。
Googleがこのように多数の国別ドメイン(ccTLD)を使い分けるのは、ユーザーの所在地に応じて最適なエッジサーバーへルーティングし、通信の低レイテンシ(低遅延)化や可用性の向上を図るための仕様です。グローバルなサービスを提供している以上、これらを適切に許可しなければ、一部のユーザー環境において正常な計測や機能提供ができないリスクがありました。
Googleドメインの統合に関する注意点(2025年4月15日以降の動向)
Googleは公式ブログにおいて、2025年4月15日より、Google検索などのサービスで国別コードトップレベルドメイン(ccTLD)を利用した地域提供を終了し、ドメインを統合していく方針を発表しています。
参照URL: https://blog.google/products-and-platforms/products/search/country-code-top-level-domains/
この発表によれば、今後はユーザーの所在地に基づいた自動的なルーティングが行われ、ccTLDへの依存は減っていくはずです。しかし、現時点での弊社のSentryレポートにおいては、依然として google.fr や google.it などの国別ドメインが頻繁に検知されているのが実情です。
公式の統合方針はあるものの、完全に移行が完了し各国のドメインへの通信が消失するまでは、網羅的な許可リストを維持しておくことが実務上の安全策といえます。
また、これらGoogle関連のドメイン以外にも本番環境特有の「ノイズ」となるレポートが確認されたのも発見でした。具体的には、ユーザーが利用しているアンチウイルスソフトによるスクリプト注入や、ブラウザ拡張機能(翻訳、広告ブロック等)による外部通信に起因する違反報告です。これらはサイト側の不備ではありませんが、実環境での運用においては、こうした「ユーザー環境由来のノイズ」と「修正すべき不備」を切り分けて分析するリテラシーが求められることを学びました。
4. ワイルドカード指定の見送りとセキュリティリスクの検討
190以上のドメインを一つずつ記述するのは非能率であると考え、当初はワイルドカードを用いた一括許可を検討しました。しかし、セキュリティ上のリスクを詳細に検討した結果、この方法は採用すべきではないという結論に至りました。
4.1 信頼モデルを崩壊させる「ドメイン偽装」のリスク
CSPの本来の目的は、実行を許可するリソースの「出所(ソース)」を厳密に特定することです。ここで曖昧なワイルドカード指定(例:*.google.* のようなイメージ)を用いてしまうと、信頼の境界線が不透明になります。
最も深刻な懸念は、「 Googleとは無関係だが、文字列にGoogleを含む悪意あるドメイン 」まで許可してしまうリスクです。
例えば、攻撃者が以下のようなドメインを取得・運用しているケースを想定します。
www.google.evil.comwww.google.security-attack.net
もし「google」という文字列を含むパターンで広範な許可を与えてしまうと、これらの悪意あるサイトへのデータ送信や、そこからのスクリプト実行をブラウザが「正当な通信」として受け入れてしまいます。これでは、XSS攻撃を防ぐための防御壁であるはずのCSPが、自らセキュリティホールを作ってしまうことになりかねません。
4.2 「明示的許可」こそが実務上の正解
「信頼できる特定の組織(Google)」のみを許可するには、不透明なパターンマッチングに頼らず、信頼が確認されたドメインのみをホワイトリストに登録するほかありません。
190件以上のドメインを列挙することは一見すると非効率ですが、「google.evil」のような偽装サイトを確実に排除し、セキュリティの整合性を維持するためには、これ以外の選択肢はないと判断しました。
5. 190以上のドメインをどう管理するか ― 保守性と可読性の課題
Googleが公式にサポートする国別ドメインは、現在190以上にのぼります(出典:Google サポートドメイン一覧)。
調査の結果、「個別に列挙するしかない」という結論に至りましたが、これをそのままミドルウェアのロジック内に記述しようとすると、以下のような問題が発生します。
5.1 【非推奨】connect-src に直接記述した場合
// 可読性が著しく低下し、ロジックが埋もれてしまう記述例
"connect-src 'self' https://www.google.com https://www.google.ad https://www.google.ae https://www.google.com.af https://www.google.com.ag https://www.google.al https://www.google.am [180ドメイン以上続く] ..."
このように「文字列の壁」がコード内に出現すると、ドメインの追加・削除といったメンテナンスが困難になるだけでなく、タイポ(打ち間違い)などの単純なミスを誘発する原因にもなります。
6. 実務的な解決策:ドメインリストの分離と運用フローの策定
この課題を解決するため、「ドメインリストを配列として独立させ、ロジックと分離する」という設計を採用しました。
6.1 配列管理による「コピペ可能」なメンテナンス
ドメイン一覧を専用の配列として切り出すことで、保守作業は劇的にシンプルになります。Googleの公式サイトから最新のドメイン一覧を取得し、エディタの置換機能などで整形して配列に貼り付けるだけで更新が完了します。
メインのポリシー生成ロジックを汚すことなく、データ(ドメインリスト)だけを最新に保つことが可能です。
6.2 運用継続性の評価(保守コストの現実的な見通し)
「世界の国々やドメインが変わるたびに更新し続けるのは大変ではないか」という懸念に対し、公的データに基づき検討を行いました。
外務省の「世界各国・地域」などの資料を確認すると、新たな国家の誕生やドメイン体系の変更は数年に一度の頻度です。
- 直近の事例: 最後に国家として承認されたのは2011年の南スーダン、最近では2024年のニウエとの国交樹立など、非常に限定的です。
したがって、「年1回程度の定期的なリスト照合フロー」を定義するだけで、実務上は十分な安全性と最新性を維持できると判断しました。
7. 実装例(Laravelミドルウェア)
コードの可読性を損なわないよう、Googleのドメインリストを専用の配列として分離し、ポリシー生成時に結合する実装を採用しました。
/**
* CSPポリシー文字列を生成する
*/
protected function getPolicy(string $nonce, string $sentryUrl): string
{
// Googleの国別ドメイン一覧
// 出典: https://www.google.com/supported_domains
$googleDomains = [
'www.google.com', 'www.google.ad', 'www.google.ae', 'www.google.com.af',
// ...(190以上のドメインを配列として定義)
'www.google.co.za', 'www.google.co.zm', 'www.google.co.zw', 'www.google.cat',
];
// 全ドメインをhttps://形式に変換
$googleUrls = array_map(fn($domain) => 'https://' . $domain, $googleDomains);
$policy = [
"default-src 'self'",
"script-src " . implode(' ', [
"'nonce-{$nonce}'",
"'strict-dynamic'",
"'self'",
'https:',
"'unsafe-eval'",
]),
// connect-srcにGoogleの全ドメインをマージ
"connect-src " . implode(' ', array_merge([
"'self'",
'https://api.stripe.com',
'https://*.google-analytics.com',
'https://analytics.google.com',
'https://www.googletagmanager.com',
'https://sentry.io',
], $googleUrls)),
"frame-src 'self' https://js.stripe.com https://www.googletagmanager.com",
"object-src 'none'",
"base-uri 'none'",
"report-uri {$sentryUrl}",
];
return implode('; ', $policy);
}
この構成(配列の分離)を採用した理由
190超のドメインを直接ポリシーに記述すると、コードの可読性が著しく低下し、ロジックが埋没してしまいます。今回、リストを配列として独立させた最大の理由は「将来的なメンテナンスコストの最小限化」です。
この構造をとることで、以下の3点を実務レベルで両立させています。
- 保守性の確保:ロジックとデータを分離し、膨大なドメインを抱えながらもコードをクリーンに保てる。
-
高いセキュリティ:ワイルドカードに頼らずドメインを特定し、
google.evil等の偽装リスクを排除できる。 - グローバル動作の担保:世界中のユーザー環境において、Google関連機能を損なうことなく提供できる。
8. おわりに
CSPの実務的な導入において、Google関連ドメインの網羅的な管理に関する知見は、日本語の公開情報が限られているのが現状です。
今回、全ドメインをミドルウェア側で明示的に管理するアプローチをとったことで、本番環境における予期せぬ機能停止リスクを最小化する土台が整いました。同様の課題に直面しているエンジニアの皆様にとって、本稿が実務的な一助となれば幸いです。
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii

