セキュリティ
Content-Security-Policy 入門:ディレクティブ、unsafe-inline、nonce、安全な導入手順
CSP は最も強力なセキュリティヘッダーであり、最も設定を誤りやすいヘッダーでもあります。重要なディレクティブ、nonce とハッシュで 'unsafe-inline' を置き換える方法、サイトを壊さない導入計画を紹介します。
このページの内容
Content-Security-Policy(CSP)は、ページがスクリプト、スタイル、画像、フレームなどのリソースをどこから読み込んでよいかをブラウザに伝えるレスポンスヘッダーです。主な役割は被害の抑制です。クロスサイトスクリプティング(XSS)のバグを突かれて攻撃者に <script> を注入されても、適切なポリシーがあればブラウザはそれを実行しません。
注意したいのは、「CSP がある」ことと「良い CSP がある」ことはまったく別だという点です。ワイルドカードだらけのヘッダーは、存在チェックには通っても、ほとんど何も守りません。このガイドでは、その違いの見分け方を説明します。
ポリシーの構成
ポリシーは、セミコロンで区切られたディレクティブの並びです。各ディレクティブは、リソースの種類と、それに対して許可するソースを指定します。たとえば Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example-cdn.com; object-src 'none' のように書きます。
ソースには、'self'(自サイトのオリジン)、https://js.stripe.com のような特定のオリジン、https: や data: のようなスキーム、'none' のようなキーワード、そして後述する nonce やハッシュを指定できます。キーワードは単一引用符で囲みますが、オリジンは囲みません。
特に重要なディレクティブ
default-src:個別に指定していないほとんどのリソースの種類に適用されるフォールバックです。script-src:スクリプトの読み込み元を指定します。実際に XSS を防ぐのはこのディレクティブなので、最も慎重に設定すべきものです。style-src、img-src、font-src、connect-src(fetch、XHR、WebSocket)、frame-src、media-src:その他のリソースの種類です。object-src 'none':<object>や<embed>などのプラグインをブロックします。古い手法ですが、スクリプト実行の経路としていまも挙げられます。base-uri 'self'または'none':注入された<base>タグによって、スクリプトの相対URLの向き先が変えられるのを防ぎます。frame-ancestors:どのサイトがあなたのページをフレームに埋め込めるかを指定します。X-Frame-Options に代わる現在の方法です。form-action:フォームの送信先を制限します。upgrade-insecure-requests:http://のサブリソースを HTTPS で読み込むようブラウザに求めます。report-to/report-uri:ブラウザが違反レポートを送信する先を指定します。
ポリシーを弱めるもの
'unsafe-inline'
script-src に 'unsafe-inline' を指定すると、すべてのインライン <script> ブロックと、onclick のようなインラインイベントハンドラーが許可されます。これはまさに XSS 攻撃が注入するものなので、スクリプトに 'unsafe-inline' を許可したポリシーでは、攻撃をほとんど食い止められません。nonce やハッシュに対応したブラウザは、それらが指定されていると 'unsafe-inline' を無視します。そのため、非常に古いブラウザ向けのフォールバックとして、nonce と併記して残されることがあります。
'unsafe-eval'
'unsafe-eval' は、eval()、new Function()、そして setTimeout への文字列引数を許可します。データをコードに変えてしまうため、危険です。古いライブラリやテンプレートエンジンの中にはこれを必要とするものもありますが、最近のビルドでは通常必要ありません。
範囲の広すぎるソース
script-src に *、https:、http:、data: を指定すると、事実上どこからのスクリプトでも許可することになり、攻撃者は任意の HTTPS サーバーに自分のスクリプトを置けます。よく使われる CDN には、もっと気づきにくい問題があります。公開 CDN のホスト全体を許可すると、攻撃者はそこでホストされている任意のライブラリを読み込めます。回避手法が知られている古いバージョンも含まれます。
Report-Only だけで終わらせる
Content-Security-Policy-Report-Only は違反を報告しますが、何もブロックしません。出発点としては正しい方法ですが、そこで止まってはいけません。
nonce とハッシュ:'unsafe-inline' から抜け出す方法
nonce は、サーバーがレスポンスごとに生成するランダムな値です。この値をヘッダーに script-src 'nonce-R4nd0mV4lue' のように記述し、信頼する各 script タグにも <script nonce="R4nd0mV4lue"> のように付けます。ブラウザは、一致する nonce を持つスクリプトだけを実行します。注入されたスクリプトはこの値を知らないので、ブロックされます。nonce は予測不可能で、レスポンスごとに異なっていなければなりません。固定の nonce は 'unsafe-inline' と変わりません。
ハッシュは、特定のインラインスクリプト1つを、その内容の SHA-256、SHA-384、SHA-512 のいずれかのハッシュ値によって許可します。script-src 'sha256-…' のように指定します。ハッシュは、レスポンスごとに nonce を生成するのが現実的でない静的なページに向いています。スクリプトを少しでも変更すると、ハッシュ値も変わります。
'strict-dynamic' を追加すると、nonce やハッシュで信頼したスクリプトがさらに別のスクリプトを読み込めるようになり、対応ブラウザにはホストの許可リストを無視するよう指示されます。これによって、タグマネージャーやウィジェットを使うサイトでも、いわゆる厳格な CSP(strict CSP)を現実的に運用できます。たとえば script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none' のように指定します。
自分の CSP がどう解釈されるかを確認する
Rudra の無料セキュリティチェッカーは、Content-Security-Policy をディレクティブごとに解析し、'unsafe-inline'、'unsafe-eval'、範囲の広すぎるスクリプトソース、object-src や base-uri の欠落を指摘します。
サイトを壊さない導入計画
- ページが読み込んでいるものを洗い出す:トップページ、ログイン、購入手続き、埋め込みコンテンツのあるページなど、主要なページで開発者ツールを開き、スクリプト、スタイル、フォント、フレーム、API のオリジンをすべて書き出します。
- Report-Only のポリシーを送信する:まずは
default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self'のようなポリシーをContent-Security-Policy-Report-Onlyで送信します。違反はブラウザのコンソールに表示され、レポート用エンドポイントを設定していればそこにも届きます。 - 違反を一つずつ修正するか、許可する:インラインスクリプトはファイルに移すか nonce を付け、インラインの
onclickハンドラーはaddEventListenerに置き換え、実際に使っているサードパーティのオリジンを個別に追加します。 - 強制する:主要な操作の流れ全体でレポートが出なくなったら、ヘッダー名を
Content-Security-Policyに切り替えます。次の段階をテストするために、より厳格な Report-Only ポリシーを並行して送信し続けることもできます。 - メンテナンスを続ける:新しいウィジェット、アクセス解析ツール、決済サービスを導入するたびに、ポリシーの変更が必要です。追加したあとは再確認しましょう。
ヘッダーは、サーバー、CDN、フレームワークのいずれかで設定し、可能な限り <meta> タグは使わないでください。meta タグのポリシーでは、frame-ancestors、レポート機能、Report-Only モードを利用できません。
Rudra がポリシーについて確認する内容
無料ウェブサイトセキュリティチェッカーは、ページが実際に送信している CSP を読み取り、解析したディレクティブを根拠として表示します。指摘するのは、ポリシーがない場合、Report-Only でしか送信されていない場合、スクリプトをまったく制限していない場合(script-src も default-src もない)、スクリプトのポリシーに 'unsafe-inline' がある場合(nonce またはハッシュがあれば指摘しません)、'unsafe-eval' がある場合、* や https: のような範囲の広すぎるスクリプトソースがある場合(nonce またはハッシュとともに 'strict-dynamic' を使っている場合を除く)です。加えて、参考情報として、object-src 'none' の欠落、base-uri の欠落、インラインスタイルも報告します。frame-ancestors は、値が * やスキームだけの指定でなければ、クリックジャッキング対策として認識されます。
チェッカーは、ポリシーが何を許可しているかを教えてくれます。しかし、そのポリシーのもとでページが正しく動き続けるかどうかはわかりません。それがわかるのは、レポート専用モードで実際の操作の流れをテストしたときだけです。
よくある質問
Content-Security-Policy は XSS を防げますか?
根本にあるバグを修正するものではありませんが、厳格なポリシーは注入されたスクリプトのほとんどを実行させないので、被害を抑えられます。出力のエスケープと入力値の検証は引き続き必要です。
style-src の 'unsafe-inline' は問題ですか?
script-src の場合に比べれば、深刻さははるかに低くなります。それでも注入されたスタイルが悪用される攻撃はあるので、取り除くことは良いセキュリティ強化になります。ただし、まずはスクリプトのポリシーを優先してください。
すべてのページで同じ nonce を使えますか?
いいえ。nonce は、レスポンスごとにランダムで一意の値でなければなりません。固定された nonce や使い回された nonce は攻撃者にコピーされてしまい、何の保護にもなりません。
強制する前に、Report-Only をどのくらいの期間動かすべきですか?
実際のトラフィックの傾向を一通りカバーできる期間が必要です。1〜2週間とすることが多く、ログイン、購入手続き、サードパーティの埋め込みコンテンツがあるページも対象に含めます。レポートに出るのがブラウザ拡張機能などによるノイズだけになったら、強制に切り替えましょう。