セキュリティ●●●○○

CSP

シーエスピー

読み込めるリソースをHTTPヘッダで宣言しXSS被害を抑える、ブラウザの防御機構。

概要

CSP(Content Security Policy)は、「このページで読み込んでよいスクリプトや画像はここから来たものだけ」という許可リストを、サーバが HTTP レスポンスヘッダで宣言し、ブラウザに強制させる仕組みです。たとえば Content-Security-Policy: script-src 'self' と宣言すれば、自サイト以外から読み込まれるスクリプトや、HTML に直接書かれたインラインスクリプトは、たとえページに紛れ込んでもブラウザが実行を拒否します。

守りたい相手の筆頭は XSS です。XSS は「攻撃者のスクリプトを被害者のブラウザで実行させる」攻撃ですから、「そもそも許可していないスクリプトはブラウザが実行しない」というルールを敷けば、注入が成立してしまった後でも被害の発火を止められます。アプリ側の対策(エスケープや入力検証)が「注入させない」ための防御だとすれば、CSP は「注入されても実行させない」ための最後の網であり、両者を重ねる多層防御の要となる語彙です。

なぜ生まれたか

XSS 対策の基本は出力時のエスケープですが、現実の Web アプリでは「すべての出力箇所で、文脈に応じた正しいエスケープを、一箇所も漏らさず行う」ことが求められます。テンプレートの書き方ひとつ、サードパーティのウィジェットひとつ、古いライブラリの脆弱性ひとつで穴が開く。つまり守る側は全箇所で勝ち続ける必要があり、攻める側は一箇所見つければよい、という非対称な戦いです。2000年代を通じて XSS が脆弱性報告の常連であり続けた事実が、「アプリ側の注意だけでは根絶できない」ことを物語っていました。

そこで発想を変え、「ブラウザ側に、実行してよいものの許可リストを渡してしまえばよい」という提案が Mozilla のエンジニアから生まれ、2010年前後に Firefox で実装、その後 W3C で標準化されました。信頼の判断をアプリの無数の出力箇所から、レスポンスヘッダ1行の宣言に集約する — 仮に注入を許しても発火を止められる保険を Web の基盤に組み込んだのが CSP です。

詳細

ディレクティブ — リソース種別ごとの許可リスト

CSP のポリシーは「ディレクティブ」の並びで書きます。リソースの種類ごとに読み込み元を指定でき、代表的なものに script-srcJavaScript)、style-srcCSS)、img-src(画像)、connect-src(fetch や WebSocket の接続先)、frame-ancestors(自分を iframe に埋め込んでよいサイト。クリックジャッキング対策)があります。個別指定のないリソースには default-src が適用されるため、まず default-src 'self'(自オリジンのみ)で全体を締め、必要な種類だけ緩める書き方が基本形です。ここでいう「オリジン」はスキーム・ホスト・ポートの組で、CORS と同じ単位ですが、役割は逆向きです。CORS が「他サイトからの読み取りを許すか」をリソース提供側が宣言するのに対し、CSP は「自ページに何を読み込むか」をページ側が宣言します。

script-src ‘self’ cdn.example ‘nonce-R4nd0m’CSP許可リスト照合自サイトの /app.js’self’ に一致実行されるcdn.example のライブラリ許可した配信元実行される注入されたインラインscriptnonce を持たない遮断 — 実行されないattacker.example のJSリストにない配信元遮断 — 実行されない
CSP はページに届いたリソースを許可リストで仕分ける — 注入されたスクリプトは実行前に遮断される

インラインスクリプトと nonce / hash

CSP を有効にすると、既定では HTML に直接書かれたインラインスクリプトと eval がすべて禁止されます。これは意図的な設計です。XSS の注入コードの大半はインラインスクリプトの形を取るため、「配信元がどこか」を判別できないインラインコードを一律に止めることが、CSP の防御力の核心だからです。とはいえ正規のインラインスクリプトが必要な場面もあります。そのための仕組みが nonce と hash です。nonce はサーバがレスポンスごとに生成する使い捨てのランダム値で、ヘッダに 'nonce-R4nd0m' と宣言し、正規の <script> タグに同じ値を属性として付けます。攻撃者はレスポンス生成前に nonce を知りようがないため、注入スクリプトには正しい値を付けられません。hash は許可したいスクリプト本文のハッシュ値をヘッダに書く方式で、内容が1文字でも変われば一致しなくなります。逆に、禁止を丸ごと解除する 'unsafe-inline' を安易に付けると CSP は事実上無力化されるため、これは「導入をあきらめた」に等しい設定だと理解しておくべきです。

Report-Only で段階導入する

実務で CSP をいきなり本番適用すると、把握していなかった正規のスクリプト(アクセス解析タグ、広告、古い管理画面のインラインコードなど)まで遮断してサイトが壊れる、というのが定番の事故です。そこで用意されているのが Content-Security-Policy-Report-Only ヘッダです。このモードではブラウザは何も遮断せず、「本適用ならこれを遮断していた」という違反レポートを report-to で指定したエンドポイントに送信するだけになります。まず Report-Only で数週間運用してレポートを収集・分析し、正規のリソースをポリシーに反映しきってから本適用に切り替える — この段階導入が実務の定石です。本適用後もレポート収集は続け、モニタリングの一部として違反を監視すれば、ポリシーの綻びだけでなく、実際の攻撃の試行を検知するセンサーとしても機能します。

落とし穴と現在の推奨

許可リスト方式には既知の抜け道があります。たとえば大手 CDN をまるごと許可すると、その CDN 上にホストされている古い JSONP エンドポイントや脆弱なライブラリを踏み台に、許可リストの内側から攻撃コードを実行される「CSP バイパス」が成立しえます。Google の調査では、ホスト名ベースの許可リストを使う実在ポリシーの大半が何らかの方法で迂回可能でした。このため現在は、配信元のホスト名を並べるのではなく、nonce と 'strict-dynamic'(nonce で信頼したスクリプトが動的に読み込む子スクリプトも信頼する指定)を軸にした「strict CSP」が推奨されています。最後に立ち位置の確認です。CSP は XSS の根本対策であるエスケープや入力検証の代替ではなく、それらが破られたときの被害を止める第二の防衛線です。また CSRF のようにスクリプト実行を必要としない攻撃には効きません。守備範囲を正しく理解した上で重ねてこそ、多層防御として機能します。