Content-Security-Policy(CSP)は、ページが使ってよいスクリプト、スタイル、画像、フレーム、通信先をブラウザーに伝えるレスポンスヘッダーです。攻撃者がHTMLにスクリプトを紛れ込ませることに成功しても、よいポリシーがあればブラウザーはそれを実行しません。ところが、ネットで見つけたポリシーをそのまま貼り付けると、たいてい二つのどちらかになります。アクセス解析やWebフォント、決済ウィジェットが動かなくなるか、エラーを消すために'unsafe-inline'やワイルドカードを足し続けて、ほとんど何でも許すポリシーになってしまうかです。
この記事では、3種類のサイト向けに実際に動く設定例を示し、ポリシーを厳格にする仕組み(nonce、ハッシュ、'strict-dynamic')、default-srcが及ばないディレクティブ、違反レポートの集め方を説明します。最後に、何も予告なく壊れないよう、Report-Onlyモードから始める導入手順を紹介します。例はすべてexample.comを使っているので、実際の値に置き換えてください。
このガイドの対象範囲
無料のチェッカーが読むのは、公開されている1つのレスポンスのヘッダーです。入力したオリジンのトップページについて、最大5回のリダイレクトをたどった後の最終レスポンスを見ます。読むのは適用中のContent-Security-Policyヘッダーだけで、Report-Onlyのポリシーは対象外です。ポリシーがサイトに合っているかまでは判断しません。また、ペネトレーションテストではありません。
1. Content-Security-Policyでできること、できないこと
ポリシーは、セミコロンで区切ったディレクティブの並びです。各ディレクティブはリソースの種類と、その読み込みを許可する送信元を指定します。JavaScriptはscript-src、CSSはstyle-src、画像はimg-src、フォントはfont-src、fetch・XHR・WebSocketの通信はconnect-src、ページに埋め込むフレームはframe-srcです。個別のディレクティブがない場合は、default-srcがこれらの取得系ディレクティブの代わりに使われます(W3C CSP Level 3)。
ただし、重要な3つのディレクティブはdefault-srcを引き継ぎません。ページをフレームに埋め込めるサイトを決めるframe-ancestors、<base>要素で設定できるURLを決めるbase-uri、フォームの送信先を決めるform-actionです。そのためdefault-src 'self'だけのポリシーでは、どのサイトでもページをiframeに埋め込めますし、注入された<base>タグで相対URLの向き先を変えられてしまいます。
送信元の書き方は3種類あります。
- キーワード(シングルクォートで囲む):
'self'(ページ自身のオリジン)、'none'(何も許可しない)、'unsafe-inline'、'unsafe-eval'、'strict-dynamic'。 - ホストとスキーム:
https://cdn.example.com、https://*.example.com、https:、data:など。 - nonceとハッシュ:
'nonce-…'や'sha256-…'。ホスト単位ではなく、個々のscript要素やstyle要素を許可します。
ポリシーはHTTPヘッダーで送ってください。<meta http-equiv="Content-Security-Policy">タグでも多くのディレクティブは使えますが、仕様上frame-ancestors、report-uri、sandboxはmetaでは使えず、Report-Onlyのポリシーはmetaタグでは一切指定できません(MDNのCSPガイド)。また、CSPは注入されたコードにできることを制限する仕組みであり、出力のエスケープや入力の検証の代わりにはなりません。注入そのものを防ぐのは後者です。
2. まず試したいContent-Security-Policyの設定例3つ
例A:外部スクリプトを使わないサイト。すべてのスクリプトとスタイルシートが自分のオリジン上のファイルであれば、許可リスト型のポリシーが使えます。
Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requestsインラインの<script>、style属性、他のホストから読み込むものはすべてブロックされます。手作りのサイトや静的サイトジェネレーターで作った、埋め込みのないサイトに向いています。
例B:サーバー側でレンダリングするサイト(nonce方式)。リクエストごとにサーバーがページを生成する場合にweb.devが推奨する厳格なポリシーに、フレーム埋め込みの保護を加えたものです。
Content-Security-Policy: script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'nonceはレスポンスごとに変えます(3章)。何を含めていないかにも注目してください。default-srcがないため、スタイル、画像、フォント、通信先は制限されません。これは意図した割り切りです。クロスサイトスクリプティングの被害が最も大きいスクリプト実行に集中し、提供元がドメインを変えるたびに壊れるホストリストを避けています。
例C:静的ページやキャッシュされるページ(ハッシュ方式)。全員に同じHTMLを返す場合は、インラインスクリプトをハッシュで許可します。
Content-Security-Policy: script-src 'sha256-BASE64_HASH_OF_INLINE_LOADER' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'| ポリシーの型 | 向いているサイト | 継続的な作業 | 注意点 |
|---|---|---|---|
| A:ホストの許可リスト | 外部のコードを使わない静的サイト | サービスを追加するたびにホストリストを更新する | 許可したホスト上のスクリプトは何でも読み込める。共有CDN上の古いライブラリも含まれる |
| B:nonce+strict-dynamic | アプリケーションがリクエストごとに生成するページ | テンプレート内のすべてのscriptタグにnonceを付ける | 同じnonceを全訪問者に返してしまうページキャッシュ |
| C:ハッシュ+strict-dynamic | 静的、プリレンダリング、エッジでキャッシュされるHTML | インラインのコードを変えるたびにハッシュを計算し直す | 空白を変えてハッシュまで変えてしまうミニファイアーやテンプレート |
どれを選ぶ場合も、6章のレポート用ディレクティブを加え、最初はContent-Security-Policy-Report-Onlyとして送ってください。
3. 'unsafe-inline'の代わりにnonceとハッシュを使う
'unsafe-inline'は、攻撃者が注入したものも含めてすべてのインラインスクリプトを許可するため、クロスサイトスクリプティング対策としてのCSPの効果をほとんど失わせます。nonceとハッシュは、自分が配信するつもりのインラインコードだけを許可します。nonceやハッシュを含むディレクティブでは、ブラウザーは'unsafe-inline'を無視します(MDN)。そのため、非常に古いブラウザー向けの予備として厳格なポリシーに残しておいても問題ありません。
nonce
nonceは、サーバーがレスポンスごとに生成する乱数です。ヘッダーに書き、実行させたいscript要素のnonce属性にも同じ値を書きます。予測できない値で、毎回新しくなければなりません。web.devは、暗号論的に安全な乱数生成器による128ビット以上の値を推奨しています。Node.jsのアプリケーションなら次のようになります。
import crypto from 'node:crypto';
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'`);
next();
});テンプレート側では同じ値で<script nonce="…" src="/app.js"></script>と書きます。nonceは、テンプレートが自分のscriptタグを出力する箇所で付けてください。完成したHTMLのすべての<script>を一括で書き換える方法は避けます。注入されたスクリプトにまで許可を与えてしまうからです。よくある落とし穴が二つあります。HTMLを保存するCDNやページキャッシュは同じnonceを全訪問者に返してしまうこと、そしてビルド時に静的ファイルへ埋め込んだ値はnonceとして機能しないことです。
ハッシュ
ハッシュは、特定のインラインスクリプト1つだけを許可します。scriptタグの間にあるテキストのSHA-256、SHA-384、SHA-512のダイジェストをbase64で表したものです。空白や改行も含めて1文字でも変わればハッシュも変わるため、ミニファイアーやテンプレートを変更すると計算し直しになります。全員に同じ内容を返す静的ページやキャッシュされるページには、ハッシュのほうが向いています。Chromeはインラインスクリプトをブロックしたとき、期待するハッシュをコンソールに表示します。自分で計算することもできます。
printf '%s' 'console.log("hello")' | openssl dgst -sha256 -binary | openssl base64どちらでも許可されないもの
onclick="…"のようなインラインのイベントハンドラーとjavascript:のURLは、nonceやハッシュを使うポリシーではブロックされます。そのコードはスクリプトファイルに移し、addEventListenerで登録してください。'unsafe-hashes'を使えば特定のハンドラーをハッシュで許可できますが、一時しのぎと考えましょう。eval()、new Function()、文字列を渡すsetTimeoutのように文字列をコードとして実行する処理には'unsafe-eval'が必要です。できるだけ書き換え、WebAssemblyのコンパイルだけが必要なら、より範囲の狭い'wasm-unsafe-eval'を使います(MDNのscript-src)。
4. strict-dynamic:信頼したスクリプトが必要なものを読み込めるようにする
多くのサイトでは、スクリプトがさらに別のスクリプトを読み込みます。タグマネージャーはアクセス解析を、同意バナーは連携先のツールを、チャットウィジェットは自前のバンドルを追加します。これらが使う可能性のあるホストをすべて列挙するのは壊れやすいやり方です。'strict-dynamic'は別の方法をとります。nonceやハッシュで許可したスクリプトは他のスクリプトを追加でき、追加されたスクリプトも信頼されます。
対応ブラウザーでは、'strict-dynamic'があるとscript-src内のホストの許可リスト、'self'、'unsafe-inline'が無視されます(MDN)。その結果、次の3点に気をつける必要があります。
- HTML内のすべての
<script>タグに、nonceか一致するハッシュが必要になります。'self'では許可されなくなるため、自分のオリジンのスクリプトも例外ではありません。 - 信頼済みのスクリプトが
document.createElement('script')で作ったスクリプトは許可されます。一方、document.writeでページに書き込まれるスクリプト(パーサーが挿入するスクリプト)は許可されないため、これに頼る古い埋め込みコードは動かなくなります。 - 信頼は引き継がれます。タグマネージャーが読み込むものはすべて実行されるので、タグマネージャーで公開できる人は、実質的にサイト上でコードを実行できる人です。その権限はデプロイ権限と同じように管理してください。
古いブラウザー向けには、新しいブラウザーが無視する予備の値を加えます。
script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none'CSP Level 3に対応したブラウザーはnonceと'strict-dynamic'だけを適用します。nonceは理解しても'strict-dynamic'を知らないブラウザーはnonceとhttps:を使い、非常に古いブラウザーは'unsafe-inline' https:に戻ります。Googleは、追加するスクリプトにnonceを引き継ぐnonce対応のタグマネージャー用スニペットを公開しており、タグマネージャーのカスタムJavaScript変数には引き続き'unsafe-eval'が必要だと説明しています(タグマネージャーのCSPガイド)。
5. frame-ancestors、object-src 'none'、base-uri:明示的に書くべきディレクティブ
frame-ancestorsは、どのサイトがページをフレームに埋め込めるかを決めるもので、クリックジャッキング対策になります。誰にも埋め込ませないなら'none'、自分のページからだけなら'self'、特定の相手ならhttps://app.example.comのように正確なオリジンを並べます。HTTPヘッダーでしか機能しません。古いブラウザー向けの予備としてX-Frame-Options(DENYまたはSAMEORIGIN)も残し、両者で許可する範囲をそろえてください(MDNのframe-ancestors)。object-src 'none'は<object>と<embed>をブロックします。最近のブラウザーにプラグインはもうありませんが、これらの要素は今でもコンテンツを読み込めます。default-srcのない厳格なポリシーでは、指定しない限り無制限のままです。base-uri 'none'(ページで<base>要素を使う場合は'self')は、注入された<base href>によって、相対URLで書かれたスクリプトがすべて別のホストを向いてしまうのを防ぎます。form-action 'self'は、フォームの送信先を制限します。決済サービスやログインサービスへ送信するフォームがあれば、そのオリジンを加え、変更後にその流れを確認してください。upgrade-insecure-requestsは、http://のサブリソースをHTTPSで取得させます。混在コンテンツ対策には役立ちますが、HSTSの代わりにはなりません。
これらのディレクティブは、例Bや例Cも含めたすべてのポリシーに明示的に書いてください。保守の手間はほとんどなく、スクリプトのルールだけでは残るすき間を埋めてくれます。
6. report-toとreport-uriで違反レポートを受け取る
違反レポートを見ると、実際の訪問者のブラウザーでポリシーが何をブロックしたか(Report-Onlyモードなら何をブロックするはずだったか)がわかります。CSP Level 3は、Reporting-Endpointsヘッダーで宣言したエンドポイントの名前を指定するreport-toを定義し、URLを直接書く従来のreport-uriを非推奨としています。MDNによると、report-toは2026年から現行のブラウザーで広く使えるようになりましたが、古いバージョンのブラウザーはreport-uriしか理解しません。report-toに対応したブラウザーはreport-uriを無視するので、当面は両方を送るのが無難です(MDNのreport-to)。
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic' 'report-sample'; object-src 'none'; base-uri 'none'; report-uri https://example.com/csp-reports; report-to csp二つの仕組みは送ってくる形式が異なります。report-uriはcsp-reportというキーを持つJSONオブジェクトを、application/csp-reportというContent-Typeで送ります。report-toはapplication/reports+jsonで、typeがcsp-violationのレポートの配列を送ります。本文にはdocumentURL、blockedURL、effectiveDirective、disposition(enforceまたはreport)などの項目が含まれます。'report-sample'を指定すると、ブロックされたインラインコードの先頭40文字もレポートに入ります。同じエンドポイントで両方の形式を受け付けるようにしてください。
- ノイズは前提にする。ブラウザー拡張機能はスクリプトを注入するため、
chrome-extension://やmoz-extension://を送信元とする違反が届きます。ディレクティブとブロックされたホストごとにまとめ、1件ずつではなく傾向を見てください。 - レポートは個人データとして扱う。ドキュメントのURLやリファラーには、メールアドレスやトークンを含むクエリ文字列が入っていることがあります。削除または短縮し、保存期間は短くして、理由なく外部に渡さないようにします。
- エンドポイントを守る。偽のレポートは誰でも送れます。サイズとレートに上限を設け、管理画面でレポートの内容をエスケープせずに表示しないでください。
7. よく壊れるもの:アクセス解析、フォント、インラインスクリプト、埋め込み
導入直後の違反のほとんどは、限られた原因から来ます。レポートのeffectiveDirectiveを見れば、どのルールを見直すべきかがわかります。
| 症状 | レポートのディレクティブ | よくある対処 |
|---|---|---|
| アクセス解析がページビューを記録しなくなった | script-src-elem、connect-src、img-src | タグをnonce付きで読み込むか、スクリプトのホストを許可し、提供元が公開している計測用ホストも許可する(Googleは製品ごとに一覧を公開しています) |
| Webフォントがシステムフォントに置き換わった | font-src、style-src-elem | スタイルシートのホストをstyle-srcに、フォントファイルのホストをfont-srcに加える(Google Fontsならfonts.googleapis.comとfonts.gstatic.com)か、フォントを自サーバーで配信する |
| テーマやCMSのスニペットが動かない | blockedURLが「inline」のscript-src-elem | テンプレートでnonceを付ける、コードをファイルに移す、またはハッシュで許可する |
onclick付きのボタンが反応しない | script-src-attr | スクリプトファイル内でaddEventListenerを使って書き直す |
| タグマネージャーのカスタム変数が値を返さない | script-src(eval) | 組み込み変数に置き換えるか、記録した例外として'unsafe-eval'を受け入れる |
| 埋め込んだ動画、地図、決済フレームが真っ白 | frame-src | 提供元の正確なオリジンを加える |
| インラインスタイルが無視され、レイアウトが崩れた | style-src-elem、style-src-attr | スタイルをスタイルシートに移す、style要素にnonceを付ける、または記録した例外としてstyle-srcだけに'unsafe-inline'を残す |
| チャットやリアルタイム更新がつながらない | connect-src | 提供元のhttps://とwss://のホストを加える |
| 自社アプリや提携先がページを埋め込めなくなった | frame-ancestors | 埋め込み元の正確なオリジンを列挙する |
インラインスタイルはインラインスクリプトよりリスクが小さいものの、無害ではありません。注入されたCSSは訪問者に見える内容を変えられますし、属性セレクターを使ってページ上のデータの一部を外に漏らすこともできます。多くのフレームワークやCSS-in-JSのライブラリーは実行時にstyle要素を挿入するので、style-srcから'unsafe-inline'を外す前に、使っているものがnonceに対応しているか確認してください。フォントやライブラリーを自サーバーで配信すれば、この表の行のいくつかがまとめて消え、ポリシーも短く保てます。
8. CSPを段階的に導入する手順
- 棚卸し。トップ、ログイン、マイページ、購入手続き、ウィジェットを埋め込んだページなど、主要なページで使っているスクリプト、スタイル、フォント、フレーム、通信先を書き出します。開発者ツールのNetworkパネルとタグマネージャーの設定を見るのが近道です。
- Report-Only。下書きのポリシーを、レポート設定付きの
Content-Security-Policy-Report-Onlyとして送ります。何もブロックされず、違反はコンソールとエンドポイントに届きます。訪問の少ないページやキャンペーンページも現れるよう、通常は1週間以上動かしておきます。 - ポリシーではなく原因を直す。nonceを付ける、インラインのハンドラーをファイルに移す、フォントを自サーバーで配信する、といった対応をします。ホストや
'unsafe-eval'を加えるのは、担当者と理由を記録した例外の場合だけにします。 - 適用。ヘッダー名を
Content-Security-Policyに変えます。次の引き締めを試すために、より厳しいReport-Onlyのポリシーを並行して送り続けましょう。両方のヘッダーが届くと、ブラウザーは一方を適用し、もう一方は報告だけを行います。 - 監視を続ける。新しいマーケティング用タグやプラグイン、フレームワークの更新で新しいインラインコードが入ることがあります。レポートのエンドポイントは動かし続け、サーバー、CDN、フレームワークを変更するたびにヘッダーを確認し直してください。
インフラ面では、二つの点が意外な不具合の原因になりがちです。CDNやフレームワークが独自のCSPヘッダーを追加すると、ブラウザーは受け取ったすべてのポリシーを適用し、リソースはそのすべてを満たす必要があります。つまりヘッダーが増えると制限は厳しくなるだけで、緩くなることはありません(MDN)。また、HTMLをエッジでキャッシュしている場合、nonce方式ではページを組み立てる場所でnonceを生成するか、ハッシュ方式に切り替える必要があります。ページが実際に送っているヘッダーは次のコマンドで確認できます。
curl -sS -D - -o /dev/null https://example.com/ | grep -i content-security-policyCSPと一緒に設定したいヘッダーはセキュリティヘッダーのチェック方法で、公開前の全体的な確認はWebサイト公開前のセキュリティチェックリストで解説しています。
9. 公開中のヘッダーを無料のチェッカーで確認する
ポリシーを適用したら、無料のセキュリティヘッダーチェッカーで、ブラウザーがトップページから実際に受け取るヘッダーを確認できます。チェッカーはSitelemetryの無料の公開準備スナップショットを実行します。登録なしで1つの公開ドメインに8項目のパッシブなチェックを行い、ヘッダーのチェックを開いた状態で他の7項目より上に表示します。
- 入力したオリジンのトップページをリクエストし、最大5回のリダイレクトをたどって最終レスポンスを読みます。ログインや購入手続きなど他のページは別のポリシーを持つことが多いので、開発者ツールやcurlで確認してください。
- 読むのは適用中の
Content-Security-Policyヘッダーだけです。Report-Onlyの段階では、報告だけのポリシーは何もブロックしないため、CSPがないものとして重要度「中」で示されます。適用に切り替えるまでは想定どおりの結果です。 - 適用中のポリシーに
frame-ancestorsがあれば、X-Frame-Optionsと同じくフレーム埋め込みの保護として扱われます。 - これとは別の堅牢化グレード(AからF)では、
default-src、またはscript-src・style-src系のディレクティブ(-elemと-attrを含む)に'unsafe-inline'がある場合や、default-srcまたはスクリプト系のディレクティブに'unsafe-eval'がある場合、CSPの配点が小さくなります。グレードは書かれたとおりにポリシーを読むので、nonceと並べた予備の'unsafe-inline'も、現行のブラウザーでは無視されるとはいえ配点を下げます。 - nonce、ハッシュ、
'strict-dynamic'、object-src、base-uri、レポート設定は評価しません。これらはブラウザーのコンソールと違反レポートで確認してください。
結果にはスコアと8項目それぞれの結果が表示されるので、CSPのシグナルをHSTSやTLSなど他の公開チェックと並べて確認できます。
よくある質問
最初に使うならどのContent-Security-Policyの設定例がよいですか?
サーバー側でレンダリングするサイトなら、script-src 'nonce-{乱数}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self' を、レスポンスごとに新しいnonceで送ります。静的ページやキャッシュされるページでは、nonceの代わりにインラインスクリプトのハッシュを使います。どちらもまずContent-Security-Policy-Report-Onlyとして始めてください。
CSPではnonceとハッシュのどちらを使うべきですか?
アプリケーションがページを毎回生成するならnonceです。そのたびに新しい値を入れられます。HTMLが静的、またはキャッシュされるならハッシュです。キャッシュされたnonceは全訪問者に使い回されてしまいます。どちらも'strict-dynamic'と組み合わせられます。
style-srcの'unsafe-inline'は問題になりますか?
script-srcの'unsafe-inline'よりリスクは小さいものの、注入されたCSSで訪問者に見える内容を変えたり、ページ上のデータの一部を漏らしたりすることは可能です。スタイルをスタイルシートに移すか、style要素にnonceを付けるまでの間、記録した例外としてだけ残してください。
Content-Security-Policy-Report-Onlyでサイトは守られますか?
いいえ。何もブロックせず、適用したポリシーならブロックするものを報告するだけです。テストに使い、その後Content-Security-Policyヘッダーに切り替えてください。両方を同時に送ることもでき、次のより厳しい版を試すときに便利です。
CSPはmetaタグで設定できますか?
多くのディレクティブは設定できます。ただし、frame-ancestors、report-uri、sandboxはmetaタグでは無視され、Report-Onlyのポリシーはmetaタグでは指定できません。サーバーやホスティングで設定できる場合は、HTTPヘッダーを使ってください。
report-uriとreport-toの違いは何ですか?
report-uriはURLを直接指定します。CSP Level 3では非推奨ですが、古いブラウザーはまだこちらに頼っています。report-toはReporting-Endpointsヘッダーで定義したエンドポイント名を指定し、Reporting APIの形式を使います。report-toに対応したブラウザーはreport-uriを無視するので、両方を送っても問題ありません。
出典・参考資料
- W3C: Content Security Policy Level 3www.w3.org
- MDN: コンテンツセキュリティポリシー(CSP)ガイドdeveloper.mozilla.org
- MDN: Content-Security-Policyヘッダーdeveloper.mozilla.org
- MDN: CSPのscript-srcディレクティブdeveloper.mozilla.org
- MDN: CSPのframe-ancestorsディレクティブdeveloper.mozilla.org
- MDN: CSPのreport-toディレクティブdeveloper.mozilla.org
- MDN: Reporting-Endpointsヘッダーdeveloper.mozilla.org
- W3C: Reporting APIwww.w3.org
- web.dev: Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)web.dev
- Google Tag Platform: Use Tag Manager with a Content Security Policydevelopers.google.com
- OWASP Content Security Policy Cheat Sheetcheatsheetseries.owasp.org
Sitelemetry編集チームが作成しています。詳しく学ぶには、掲載した参考資料をご覧ください。



