コンテンツセキュリティポリシー(CSP)とは?nonce方式の設定とXSS防御の実装判断を解説
CSPは、そのページで実行してよいスクリプトの出所をブラウザへ宣言するHTTPヘッダーです。宣言から外れたスクリプトは、たとえ攻撃者に注入されていてもブラウザが実行を拒みます。この記事では、ディレクティブの構文とmetaタグ配信の制限、ドメインを並べる許可リスト方式が実測で破られてきた事情、nonceと’strict-dynamic’を組み合わせた設計、Report-Onlyから本適用へ進める段階手順、nginxとExpressでのnonce生成、タグマネージャが止まったときの切り分けまでを実装視点でまとめました。CSPでは塞げない範囲と、そこを何に任せるかの線引きも示します。
まとめ|CSPを導入する条件とnonce方式へ寄せる実装の結論
結論を先に置きます。これから書くCSPで、ドメインを列挙するホスト許可リスト方式を選ぶ理由はありません。Googleが約168万ホストを解析した2016年の調査では、スクリプト実行を制限しようとしたポリシーの94.68%が実際には回避可能でした。許可したCDNの中に危険なエンドポイントが1つ混ざれば、その時点でポリシーは形だけになります。書くなら nonce 方式です。
既存サイトへ後付けする場合の順序は決まっています。まず Content-Security-Policy-Report-Only で1〜2週間ぶん違反を集め、自社のスクリプトがどこから読まれているかを実測する。次にテンプレート側をnonce対応へ書き換える。強制適用はその後です。順番を逆にすると、決済フォームやチャットウィジェットが本番で沈黙します。
見送る判断もあります。テンプレートに手を入れられない外部SaaS上のサイトでは、nonceをリクエストごとに差し込めません。この状態で許可リスト方式のCSPだけ載せても、防御としてはほぼ機能しない飾りになります。
コンテンツセキュリティポリシーの定義とブラウザが読み込みを遮断する仕組みの範囲
仕組みそのものは単純です。サーバがルールを宣言し、ブラウザがそれを守る。守らせる相手はブラウザであって、攻撃者ではありません。
Content-Security-Policyヘッダーの構文とディレクティブの記述順
CSPはレスポンスヘッダー1本で表現します。ディレクティブ名のあとに空白区切りでソース式を並べ、ディレクティブ同士はセミコロンで区切る形式です。
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.jp; img-src 'self' data:;
記述順に優先度はありません。同じディレクティブを2回書いた場合は、後ろではなく最初の指定が採用され、2回目は無視されます。一方、ヘッダー自体を複数本返した場合の扱いは逆で、すべてのポリシーが個別に評価され、いずれか1本でも禁じたリソースは読み込まれません。ヘッダーを追加すると防御は緩まず、必ず厳しくなる方向にしか動かない、と覚えておくと事故が減ります。仕様は W3C の Content Security Policy Level 3 で、2026年7月29日版が Working Draft として公開されています。
インラインスクリプトの実行を止めてXSSの成立を断つ判定の原理
反射型・格納型を問わず、XSSの成立には「注入した文字列がスクリプトとして実行される」という最後の一段が必要です。CSPの判定対象は、その段階だけです。script-src を宣言すると、ブラウザは既定でインラインの <script> ブロックと onclick などのイベントハンドラ属性、javascript: URLの実行を拒みます。攻撃者が入力欄からタグを流し込むところまでは成功しても、実行のフェーズでブラウザが降ります。
裏を返せば、CSPは注入そのものを止めません。エスケープ漏れは残ったままで、被害の発火だけが抑えられている状態です。攻撃側の原理と、出力エスケープやトークン検証といった本体側の対策はXSSとCSRFの仕組み・被害・対策を実装レベルで比較した記事で整理しているので、CSPを二重の網として重ねる前提で読み分けてください。
HTTPヘッダーでの配信とmetaタグ配信で使える指定の違い
CSPは <meta http-equiv="Content-Security-Policy" content="..."> でも配信できますが、使える指定が減ります。frame-ancestors、report-uri、sandbox はmeta経由では無視され、Content-Security-Policy-Report-Only に至ってはmetaでの配信自体ができません。試験運用がまるごと成立しないため、実務ではヘッダー配信が前提になります。
もう一つ、metaタグは <head> のパース位置より前に読まれたリソースへ効きません。ポリシーが有効になる前に外部スクリプトが1本走ってしまう余地が残ります。同じくブラウザ側で効くヘッダー制御でも、クロスオリジンの読み取り可否を扱うCORSの仕組みとサーバー設定例とは守備範囲が別で、CORSは「他オリジンのリソースを読んでよいか」、CSPは「このページが何を実行してよいか」を決めます。両方を同じ設定ファイルで扱うことになるため、役割を混ぜないでください。
CSPの主要ディレクティブ一覧とソース式で指定できる値の種類と優先順位
ディレクティブは40種類近くありますが、最初に押さえるのは5つで足ります。残りは違反レポートを見ながら足していく順序で構いません。
default-srcとscript-src・frame-ancestorsの役割分担
default-src は個別指定のないフェッチディレクティブの既定値になります。ただし frame-ancestors、base-uri、form-action は default-src の傘に入らないため、別途書かないと無指定のままです。ここは取りこぼしが起きやすい箇所です。
| ディレクティブ | 制御対象 | default-srcの継承 | 実務での初期値 |
|---|---|---|---|
| default-src | フェッチ全般の既定値 | — | ‘self’ |
| script-src | JavaScriptの読み込みと実行 | する | nonce+strict-dynamic |
| style-src | CSSの読み込みと適用 | する | ‘self’(インラインは段階移行) |
| frame-ancestors | 自ページを埋め込める親 | しない | ‘none’ または ‘self’ |
| object-src | object・embed要素 | する | ‘none’ |
frame-ancestors 'none' は X-Frame-Options の後継にあたる指定で、クリックジャッキング対策として先に入れておく価値があります。攻撃が成立する条件と、埋め込みを許す相手を絞る書き方はクリックジャッキングとframe-ancestorsによる防御の実装解説にまとめました。継承しない3つのうち、実害が出やすいのはこれと base-uri です。
‘self’・’unsafe-inline’とnonce・hashによる指定の違い
ソース式には、オリジン指定(https://cdn.example.jp)、キーワード('self' 'none')、スキーム指定(data: https:)、そして nonce と hash があります。実装判断で効いてくるのは後ろの2つです。
nonce はレスポンスごとに生成する使い捨ての乱数で、ヘッダー側に 'nonce-r4nd0m' と書き、同じ値を <script nonce="r4nd0m"> として出力します。値が一致したスクリプトだけが走る仕組みなので、動的にHTMLを組み立てるアプリと相性が良い。hash はスクリプト本文のSHA-256/384/512ダイジェストをBase64で書く方式で、内容が1バイト変わるたびに再計算が必要になります。静的サイトジェネレータでビルド時に埋め込むなら hash、サーバがレスポンスを組み立てるなら nonce、という切り分けが実務的です。
'unsafe-inline' を書くとインライン実行がすべて許可され、CSPのXSS防御はその瞬間に無効化されます。nonce や hash と併記した場合、それらを解釈できるブラウザは ‘unsafe-inline’ を無視するため、旧環境向けのフォールバックとして残す使い方だけが許容されます。
ホスト許可リスト方式が回避される理由とstrict CSPへの移行判断
CSPを入れたのに効いていないサイトは珍しくありません。原因はほぼ一つ、許可リストの書き方にあります。
Google調査が示した94.68%という許可リストの無効化率
Weichselbaum らが ACM CCS 2016 で発表した論文「CSP Is Dead, Long Live CSP!」は、1,680,867ホスト・26,011の一意なポリシーを解析しました。結果は厳しいものでした。スクリプト実行を制限しようとしたポリシーの94.68%が回避可能で、CSPを持つホストの99.34%はXSSに対して効果を持っていませんでした。
理由は許可リストの中身にあります。スクリプト読み込み先として最も多く許可されていた15ドメインのうち14ドメインが、JSONPエンドポイントや古いAngularライブラリなど、任意コード実行に転用できる経路を抱えていました。つまり自社が広告配信やタグ配信のドメインを1つ許可した時点で、そのドメイン上の未知のエンドポイントも一緒に許可したことになる。一意なポリシーの75.81%がこの穴を持っていた、というのがこの調査の実測値です。同論文が提案した回避策が ‘strict-dynamic’ でした。
nonceと’strict-dynamic’を組み合わせた推奨ポリシーの構成
web.dev が示す推奨形は次の1行です。
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
ドメインが1つも出てこない点に注目してください。許可の根拠をドメインから「サーバが発行した乱数を持っているか」へ移したため、どのCDNが汚染されても入口が開きません。'strict-dynamic' は、nonce付きで実行が許可されたスクリプトが動的に読み込む子スクリプトへ信頼を伝播させるキーワードです。これがないと、タグマネージャのように自分でスクリプトを注入するライブラリがすべて止まります。
一方、信頼の伝播には代償が伴う点に注意が必要です。nonceを与えたスクリプトが乗っ取られれば、そこから読まれるものは無条件で通ります。自社管理下のバンドルにだけnonceを付け、外部SDKは ‘strict-dynamic’ の伝播経由で読ませる設計にしておくと、被害の起点を自社コードに閉じ込められます。
object-srcとbase-uriを’none’で塞ぐ必要がある攻撃経路
この2行は「書かないと nonce 方式が迂回される」ために存在します。飾りではありません。
object-src 'none' は <object> や <embed> を通じたプラグイン読み込みを塞ぎます。script-src をどれだけ厳しくしても、プラグイン経由で任意のコンテンツを実行される経路が残っていては意味がありません。base-uri 'none' のほうはより直接的で、<base href="https://attacker.example/"> を注入されると、ページ内の相対URLで書かれたスクリプトの解決先がまるごと攻撃者のサーバへ向きます。nonceは要素に付いたままなので、中身だけがすり替わる。base-uri は default-src の継承対象外なので、明示的に書かない限り無防備のままです。
Report-Onlyから本適用へ進める段階導入の手順と違反レポートの受け方
稼働中のサイトへいきなり強制適用すると、まず問い合わせフォームが壊れます。手順を踏んでください。
Report-Onlyヘッダーで違反を集める初期運用の進め方
Content-Security-Policy-Report-Only は、違反を検出しても実行はブロックせず、レポートだけを送るヘッダーです。同一レスポンスに強制版と併記でき、「今の本番ポリシーは維持しつつ、次のポリシー案を試験する」という二重運用ができます。
- Report-Only で
default-src 'self'; script-src 'nonce-{RANDOM}' 'strict-dynamic'を配信し、1〜2週間ぶんの違反を蓄積する - 集まった blocked-uri を、自社資産・契約済み外部サービス・身に覚えのないものの3つに仕分ける
- 自社資産のインラインスクリプトをnonce対応へ書き換え、外部サービスは読み込み方法を確認する
- 違反が想定内だけになった段階で、同じポリシーを Content-Security-Policy へ移す
- 強制適用後も Report-Only を1段厳しいポリシーで走らせ、次の締め上げに備える
2の仕分けで身に覚えのないドメインが出てきたら、その時点で調査対象です。広告SDKが呼んでいるだけのこともあれば、既に注入されている場合もあります。
サーバ側でnonceを毎リクエスト生成するnginxとExpressの実装
nonceは推測不能であることが前提なので、リクエストごとに新しい値を生成します。ページをキャッシュして同じnonceを使い回した瞬間、攻撃者は正解の値を知ることになり、防御は消えます。CDNのフルページキャッシュとnonce方式が衝突するのはこの理由です。
nginx 単体なら ngx_http_sub_module と $request_id を組み合わせてHTML中のプレースホルダを置換する方法があります。ただし置換対象を取りこぼしやすいため、アプリケーション側で発行するほうが確実です。Express なら、リクエストごとに16バイトの乱数を作ってレスポンスローカルへ置き、テンプレートから参照します。
res.locals.nonce = crypto.randomBytes(16).toString('base64') の値をヘッダー文字列とテンプレートの両方へ渡す形です。仕様は128ビット以上の予測不能な値を求めており、16バイトはその下限に合わせた寸法にあたります。
report-toとreport-uriの併記が必要になるブラウザ対応の現状
違反レポートの送り先指定は、旧来の report-uri から Reporting API ベースの report-to へ移行中です。report-to は単体では動かず、Reporting-Endpoints: csp-endpoint="https://example.jp/csp-reports" というヘッダーで名前付きエンドポイントを定義してから、CSP側で report-to csp-endpoint と参照します。
受け取るJSONの形式も違います。report-uri は Content-Type: application/csp-report、Reporting API 側は application/reports+json で type: "csp-violation" を含む構造です。受信エンドポイントを自作するなら両方を解釈できるようにしておいてください。
MDN の互換性情報では、report-to は2026年3月時点で主要ブラウザの最新版において利用可能となりました。ただし旧バージョンの端末が残る一般向けサイトでは、両方を併記する運用が続きます。report-to を解釈できるブラウザは report-uri を無視する仕様なので、併記による二重送信は起きません。
導入後に壊れやすい箇所とCSPだけでは塞げない範囲の実装判断
CSPの運用コストは、初期設定よりも「入れたあとに何かが動かなくなる」対応に集中します。壊れる箇所には偏りがあります。
GTM・GA4など外部タグが読み込まれなくなったときの切り分け
タグマネージャ配下の計測タグが消えるのが典型例です。GTMは自身がスクリプトを動的に注入して動くため、nonceだけを許可して ‘strict-dynamic’ を書かないと、GTM本体は動いても配下のタグが全滅します。DevToolsのコンソールに Refused to load the script の行が出ているか、blocked-uri がどのドメインかをまず確認してください。
切り分けの順序は、ブロックされたのが「読み込み」か「実行」かの判別から入ります。外部URLが blocked-uri に出ていれば読み込み段階なので script-src の指定漏れ、blocked-uri: inline なら実行段階なのでnonceの付け忘れです。カスタムHTMLタグ内のインラインスクリプトはnonceを付けられないため、’strict-dynamic’ で伝播させるか、タグ自体を外部ファイル化する二択になります。プレビューモードで通っても本番で落ちる場合は、キャッシュ層がnonce付きHTMLを使い回している可能性を疑ってください。
CSPで防げない攻撃を脆弱性診断・WAFへ振り分ける判断の基準
CSPが効くのはブラウザの中だけです。SQLインジェクション、認可の抜け、サーバ側のRCEには一切関与しません。攻撃者がブラウザを使わずAPIを直接叩けば、ポリシーは1行も評価されないまま処理が通ります。
役割分担で言えば、リクエストがサーバへ届く前に既知パターンで弾くのがWAFの仕組みと選び方の領域、アプリケーション内部の処理を監視して実行直前に止めるのがRASPによる実行時防御の領域です。CSPはそのどちらでもなく、被害の発火点であるブラウザ側に最後の網を張る層にあたります。3つは代替関係ではなく、止める場所が違うだけです。
そして、どの層も「そもそも脆弱性がどこに何本あるか」を教えてはくれません。エスケープ漏れの棚卸しと、CSPが実際に効いているかの検証には手動を含む診断が要ります。自社の実装で穴が塞げているかを外部の目で確認したい場合は、脆弱性診断・セキュリティ診断で対象範囲と手法を相談してください。ポリシーの記述だけを直しても、注入経路が残っていれば守りは1枚のままです。
よくある質問
CSPの設定で実装者から出やすい疑問を、判断に直結する順で5つ挙げます。
CSPを入れればXSS対策は不要になりますか?
なりません。CSPが止めるのは注入された文字列の実行であって、注入そのものではありません。エスケープ漏れが残っていれば、CSPを解釈しない環境や、ポリシーの穴を突かれた場合にそのまま成立します。出力エスケープとテンプレートエンジンの自動エスケープを本体の対策とし、CSPは失敗したときの二重の網として重ねる位置づけで設計してください。順序を逆にすると、防御全体が1枚のポリシー文字列に依存します。
nonceとhashはどちらを選べばよいですか?
サーバがレスポンスごとにHTMLを組み立てるアプリなら nonce、ビルド時にHTMLが固まる静的サイトなら hash です。判断基準は「レスポンスごとに値を差し替えられるか」の一点にあります。hash はスクリプト本文が1バイト変わるたびに再計算が要るため、動的な出力には向きません。逆にnonceはフルページキャッシュと衝突するので、CDNでHTMLごとキャッシュしている構成なら hash を検討してください。
CSPを設定するとページの表示速度は落ちますか?
体感できるほどの低下は起きません。ポリシーの評価はブラウザ内で完結し、ネットワーク往復が増えないためです。影響が出るとすればnonce方式によるキャッシュ設計の変更で、HTMLをCDNでキャッシュできなくなる分の応答時間が伸びます。HTMLだけキャッシュ対象から外し、静的アセットは従来どおりキャッシュする構成にすれば、実用上の差はほぼ吸収できます。
Report-Onlyのまま運用し続けても意味はありますか?
防御としての意味はありません。Report-Only は違反を報告するだけで、スクリプトの実行は許可されたままです。ただし監視としての価値は残ります。想定外のドメインからスクリプトが読み込まれ始めたことを検知できるため、強制適用が難しい環境では改ざん検知の目として置く選択はありえます。防御を期待するなら、期限を切って本適用へ移してください。
WordPressのような既存CMSでもnonce方式は導入できますか?
テーマとプラグインの改修を伴います。WordPressはプラグインが独自にインラインスクリプトを出力するため、wp_add_inline_script を経由しない出力が残っていると、そのプラグインの機能だけが止まります。まずReport-Onlyでどのプラグインが引っかかるかを洗い出し、代替の効くものは差し替え、効かないものは hash 指定で個別に許可する進め方が現実的です。プラグイン更新のたびにhashの再計算が要る点は運用コストとして見込んでください。
関連記事
- DASTとは?SASTとの違い・ZAPでのCI/CD組み込みと導入判断を実装視点で解説:CSPを重ねる前にXSSそのものを出荷前へ見つける工程
- APIセキュリティとは?OWASP API Top 10の主要リスクと実装対策を解説:ブラウザを経由しない攻撃面の守り方
- Invalid CSRF tokenの意味と直し方|Laravel 419・Django・Spring Securityのエラーを切り分ける:トークン方式で起きる実装トラブルの切り分け