CSP(Content Security Policy)入門 〜 XSS対策の最後の防波堤を理解する
CSPとは何か、どんなディレクティブがあるか、HTTPヘッダーとmetaタグどちらで設定すべきか、nonce/hashによるインラインスクリプト許可、Report-Onlyによる安全な導入手順まで、CSPの全体像をまとめました。
はじめに
XSS対策としてCSPを導入しようとすると、「script-srcって何?」「nonceってどう使うの?」というところで一気に難易度が上がります。
HTTPヘッダーを追加するだけで終わるように見えますが、実際にはインラインスクリプトや外部ライブラリが引っ掛かり、「画面が動かなくなった」「見た目が崩れた」といった問題に遭遇することも少なくありません。
CSP(Content Security Policy)は、入力値のエスケープやサニタイズとは異なるアプローチでXSSを防ぐ仕組みです。もし悪意のあるスクリプトがページに入り込んでしまっても、ブラウザ側で実行を止められるため、「最後の防波堤」と呼ばれることがあります。
この記事では、CSPの基本的な仕組みだけでなく、導入時につまずきやすいポイントや運用時の注意点についても紹介します。
CSPとは
CSPは、HTTPレスポンスヘッダー(Content-Security-Policy)を通じて、ブラウザに対して「どこから読み込んだリソースだけを信頼するか」を宣言する仕組みです。
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
この例では、JavaScriptは自分自身のオリジン('self')とhttps://cdn.example.comからのみ読み込めます。
仮に攻撃者がHTMLへ<script>タグを埋め込めたとしても、読み込み元やインラインコードがCSPに違反していれば、ブラウザが実行を拒否します。
「XSSを発生させない」のではなく、「発生してしまっても被害を抑える」のがCSPの役割です。
主要なディレクティブ
CSPでは、リソースの種類ごとにポリシーを設定します。
| ディレクティブ | 対象 |
|---|---|
default-src | 個別指定がないディレクティブのフォールバック |
script-src | JavaScript |
style-src | CSS |
img-src | 画像 |
connect-src | fetch、XMLHttpRequest、WebSocketなど |
font-src | フォント |
frame-src | <iframe>の読み込み先 |
frame-ancestors | このページを埋め込める親ページ |
form-action | <form>の送信先 |
object-src | <object>、<embed>(通常は'none'推奨) |
指定できる値の代表例です。
| 値 | 意味 |
|---|---|
'self' | 自分自身のオリジン |
'none' | 全て禁止 |
https://example.com | 特定のオリジンのみ許可 |
'unsafe-inline' | インラインコードを許可(極力避ける) |
'unsafe-eval' | eval()などを許可(極力避ける) |
'nonce-xxxx' | nonceが一致するタグのみ許可 |
'sha256-xxxx' | ハッシュが一致するコードのみ許可 |
default-srcは「あらゆるリソースを制御する設定」ではありません。
あくまでフォールバックなので、script-srcやimg-srcなどが指定されていれば、そちらが優先されます。そのため、多くのプロジェクトではリソースごとに明示的に指定します。
また、CSPはscript-srcだけ設定すれば終わりではありません。
例えばconnect-srcを設定しておけば、JavaScriptが任意のサーバーへfetchやWebSocketで通信することも制限できます。利用しているリソース全体を整理した上でポリシーを設計することが重要です。
HTTPヘッダーとmetaタグ
CSPはHTTPレスポンスヘッダーで設定するのが基本ですが、metaタグでも指定できます。
<meta http-equiv="Content-Security-Policy" content="default-src 'self'" />
ただし両者は完全に同じではありません。
frame-ancestorsやsandboxなど、一部のディレクティブはHTTPヘッダーでしか利用できません。
サーバー側でヘッダーを設定できるのであれば、基本的にはHTTPヘッダーを利用しましょう。metaタグは静的ホスティングなど、ヘッダーを変更できない環境での代替手段と考えるのがよいでしょう。
インラインスクリプトとnonce / hash
CSPを導入すると、多くの人が最初にぶつかるのがインラインスクリプトです。
<script>
console.log("inline script");
</script>
このようなコードは、'unsafe-inline'を許可しない限り実行されません。
もちろん'unsafe-inline'を指定すれば動くようになります。しかし、それでは攻撃者が埋め込んだインラインスクリプトまで許可してしまい、CSPを導入する意味がほとんどなくなります。
そこで利用されるのがnonceです。
<script nonce="rAnd0mValue123">
console.log("実行される");
</script>
Content-Security-Policy: script-src 'nonce-rAnd0mValue123'
サーバーはリクエストごとにランダムな値を生成し、HTTPヘッダーと<script>タグの両方へ埋め込みます。
攻撃者はその値を事前に知ることができないため、スクリプトを注入できても実行できません。
なお、nonceはリクエストごとに毎回異なる値である必要があります。そのためSSR系のフレームワークでは比較的導入しやすい一方、静的HTMLだけで配信しているサイトでは工夫が必要になります。
静的なインラインスクリプトであれば、hashを使う方法もあります。
Content-Security-Policy: script-src 'sha256-<ハッシュ値>'
こちらはスクリプト内容からSHA-256ハッシュを計算して許可します。
ただし、コードが1文字でも変わればハッシュ値も変わるため、頻繁に変更されるコードにはあまり向いていません。
Report-Onlyで安全に導入する
既存システムへいきなりCSPを適用すると、思わぬところで画面が壊れることがあります。
そのため、多くの場合はContent-Security-Policy-Report-Onlyから導入します。
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
このモードではブラウザはブロックを行わず、違反だけを報告します。
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src",
"blocked-uri": "https://evil.example.com/inject.js"
}
}
実際の運用では、最初から厳しいポリシーを設定するのではなく、
default-src 'self'script-src 'self'style-src 'self'
あたりから始め、違反レポートを見ながら必要な許可を追加していくケースが一般的です。
既存システムでは、一度で全ての違反を解消するのは難しいことも珍しくありません。段階的に適用していく方が安全です。
つまずきやすいポイント
サードパーティスクリプトが増えるほど管理が大変になる
Google AnalyticsやGoogle Tag Manager、決済サービス、チャットウィジェットなどを導入すると、その都度許可するドメインが増えていきます。
さらに、そのライブラリ自身が別ドメインからスクリプトや画像を読み込むこともあり、思った以上に許可リストが増えることがあります。
フレームワークがインラインコードを出力していることがある
ReactやVueを使っているからといって、インラインスクリプトが存在しないとは限りません。
Next.jsやNuxt、Vite、Google Tag Managerなどは、内部でインラインコードを生成することがあります。
「自分では書いていないのにCSP違反になる」というケースもあるため、生成されたHTMLまで確認する必要があります。
inline styleも意外と多い
JavaScriptだけでなく、CSSでも同じ問題が起こります。
UIライブラリによってはJavaScriptから動的にstyle属性を書き換えるものもあり、style-src 'self'だけではレイアウトが崩れることがあります。
まずはReport-Onlyで実態を確認し、必要に応じて調整するのがおすすめです。
strict-dynamicは便利だが理解して使う
strict-dynamicを使うと、nonceで許可されたスクリプトが動的に読み込む子スクリプトも信頼されます。
Content-Security-Policy: script-src 'nonce-xxxx' 'strict-dynamic'
動的import()を多用するモダンなフロントエンドでは便利ですが、script-srcのホワイトリストの扱いが変わるため、仕組みを理解した上で利用しましょう。
まとめ
- CSPはXSSを防ぐ最後の防御層として機能する
default-srcはフォールバックであり、リソースごとに明示的に設定するのが基本- HTTPヘッダーで設定するのが推奨
- インラインスクリプトは
nonceまたはhashで許可する - いきなり適用せず、まずはReport-Onlyで違反を確認する
- 外部ライブラリやフレームワークが意外なところでCSP違反を起こすことがある
CSPは、一度設定すれば終わりという仕組みではありません。新しいライブラリや外部サービスを追加したときには、ポリシーも見直す必要があります。
また、CSPだけでXSSを防げるわけでもありません。入力値のエスケープやサニタイズ、HttpOnly Cookieなど、他の対策と組み合わせることで効果を発揮します。
導入のハードルは決して低くありませんが、「もしXSSが発生してしまったら」という最後の備えとして、取り入れる価値は十分にあると思います。