コンテントセキュリティポリシー入門

Chromeの拡張機能はmanifest v2からCSP対応とかでいろいろと制限が厳しくなったわけですが、そもそもそのCSPがよく分からなかったので、HTML5Rocksの入門記事を訳してみました。

ところどころよく分からなくて適当に訳してたりするので、おやっ?と思ったら原文参照のこと。


http://www.html5rocks.com/en/tutorials/security/content-security-policy/

コンテントセキュリティポリシー入門

注: この記事はまだ完全に標準化を終えておらず不安定なAPIについて述べています。自身のプロジェクトで実験的なAPIを使う場合には注意が必要です。

ウェブのセキュリティモデルは同一生成元ポリシーにその根拠を持ちます。 https://mybank.com のコードは https://mybank.com のデータにだけアクセスすべきですし、https://evil.example.com がアクセスを認められることはあってはいけません。それぞれの生成元はウェブの他の部分から独立を保たれ、開発者が開発したり遊んだりできる安全なサンドボックスを与えてくれます。理論的にはこれは完璧です。しかし実際には、攻撃者はシステムを破壊する巧妙な手段を見つけ出します。

例えば、クロスサイトスクリプティング(XSS)攻撃は意図したコンテンツと一緒に悪意のあるコードを送り込むことでサイトを騙し、同一生成元ポリシーをバイパスします。ブラウザはあるページ内のすべてのコードを合法的にそのページのセキュリティポリシーの一部に含まれると信用するので、これは非常に大きな問題です。XSSチートシートは攻撃者が悪意のあるコードを送り込むことでこの信頼を破るために使用するかもしれない手段の、古いけれど代表的な例があります。いずれにしても攻撃者が何かしらのコードを送り込むことに成功すると、ほぼゲームオーバーです。ユーザーセッションのデータは危険に晒され、秘密であるべき情報はThe Bad Guys(tm)に読み取られてしまいます。我々はこれを可能な限り禁止したいと思っているのです。

このチュートリアルは、モダンブラウザでのXSS攻撃のリスクとインパクトを大幅に減らすと期待される新しい防御方法に焦点を当てます。それがコンテントセキュリティポリシー(CSP)です。

ソースホワイトリスト

XSS攻撃者によって悪用される主な問題点は、ブラウザがアプリケーションの一部とみなされるスクリプトと、悪意を持ってサードパーティから送り込まれたコードを区別することができないということです。例えば、この記事の一番上にあるGoogle +1ボタンはこのページ生成元と同じコンテキストで https://apis.google.com/js/plusone.js からコードを読み込んで実行します。我々はこのコードを信用しますが、ブラウザ自身がapis.google.comのコードはすばらしいけれどapis.evil.example.comのコードはおそらくそうではないと判断すると期待することはできません。ブラウザは脳天気にもどこから取ってきたかに関わらずリクエストされたページのあらゆるコードをダウンロードして実行します。

サーバーから受け取った全てのものを盲目的に信用する代わりに、CSPはContent-Security-Policy HTTPヘッダを定義し、そこで信用できるコンテンツのソースのホワイトリストを作成して、ブラウザにそれらのソースから得たリソースだけを実行したり表示したりするよう指示できるようにします。攻撃者がスクリプトを注入できる穴を見つけたとしても、そのスクリプトがホワイトリストに含まれていなければ、実行されません。

ここではapis.google.comが正当なコードを配布していると信用し、また我々自身のことも同様に信用しているので、それら二つのソースのいずれかから取得されたスクリプトだけを実行するようにポリシーを定義してみましょう:

Content-Security-Policy: script-src 'self' https://apis.google.com

簡単でしょう?おそらく予想通り、script-srcは特定のページのスクリプトに関連した特権のセットを制御するためのディレクティブです。今回はスクリプトの正当なソースとして'self'とhttps://apis.google.comを指定しています。ブラウザはきちんとapis.google.comからHTTPSを用いてJavaScriptをダウンロードして実行します。現在のページのオリジンの場合も同様です。

このポリシーが定義されていると、その他のソースからスクリプトをロード使用としても単純にエラーが投げられて終ります。頭のいい攻撃者があなたのサイトにどうにかコードを注入したとしても、期待した成功ではなくその正反対のエラーメッセージを見ることになるでしょう。

様々な種類のリソースに適用できるポリシー

スクリプトのリソースは明らかに最大のセキュリティリスクですが、CSPはページがロードを許されるリソースを適正な粒度で制御できる豊富なポリシーディレクティブのセットを提供しています。すでにscript-srcを目にしているので、コンセプトは理解しているでしょう。残りのリソースディレクティブをざっと見てみましょう。

  • connect-srcは(XHR、WebSockets、EventSourceを通じて)接続できるオリジンを制限します。
  • font-srcはウェブフォントを配布できるオリジンを指定します。Googleのウェブフォントは次の指定で有効化できます。font-src https://themes.googleusercontent.com
  • frame-srcはフレームとして組み込めるオリジンの一覧を指定します。例えば次のように指定すればYouTubeは組み込めますが、それ以外のオリジンは組み込めません。frame-src https://youtube.com
  • img-srcは画像を取得できるオリジンを定義します。
  • media-srcは動画や音楽を配布できるオリジンを限定します。
  • object-srcはFlashやその他のプラグイン経由でのコントロールを許可します。
  • style-srcはスタイルシートのためのscript-srcのカウンターパートです。

デフォルトでは、ディレクティブは広く開かれています。ディレクティブに特定のポリシーを指定しなければ、例えばfont-srcのように指定すれば、有効なソースとして*を指定したものとして動作します。(つまり、あらゆる場所から制限無くフォントをロードできます)

デフォルトの動作はdefault-srcディレクティブを指定することで上書きできます。このディレクティブは、想像通り、なにも指定しなかったディレクティブのデフォルト値を定義します。default-srcがhttps://example.comに設定されていて、font-srcディレクティブの指定に失敗していれば、フォントはhttps://example.comからはロードできますが、それ以外はどこからもロードできません。先の例ではscript-srcだけを指定したので、画像やフォントなどはあらゆるオリジンのものをロードできます。

HTTPヘッダの中で、セミコロンで各ディレクティブを分割して並べるだけで、特定のアプリケーションに必要なだけディレクティブを利用できます。一つのディレクティブ内で特定のタイプの必要なリソースを全て並べているか確認したくなるでしょう。script-src https://host1.com; script-src https://host2.comのように書くと、二つ目のディレクティブはそのまま無視されます。script-src https://host1.com https://host2.comが二つのオリジンを正当なものであると指定する正しい方法です。

例えば、もし全てのリソースをコンテンツデリバリネットワーク(https://cdn.example.netとします)からロードするアプリケーションがあるとして、フレームやプラグインが一切不要だと分かっていれば、ポリシーは次のようになるでしょう。

Content-Security-Policy: default-src https://cdn.example.net; frame-src 'none'; object-src 'none'

実装の詳細

先に進む前に、例で使用していた標準的なヘッダContent-Security-Policyについての重要な注意があります。現在のブラウザはプレフィクス付きで機能を実装しています。FirefoxはX-Content-Security-Policyで、Webkitベースのブラウザ(SafariとChrome)はX-Webkit-CSPを使用します。実装はほぼ同じですが、近いうちに標準と置き換えられるでしょう。本記事ではContent-Security-Policyを継続して使用します。現時点ではプレフィクスは必須ですが、いずれブラウザがそのヘッダを置き換えるでしょう。

使用するヘッダに関わらずポリシーはページごとに定義されるため、あなたが確実に守りたと思う全てのレスポンスにHTTPヘッダをつけて送信する必要があります。これにより大きな柔軟性が得られ、特定のページのために特別な必要性に応じて調節されたポリシーを設定できます。例えばサイト内に+1ボタンを持つページ群とそうではないページ群があるときに、必要な場所でだけボタンのコードを許可できます。

それぞれにディレクティブに含めることのできるソースリストは十分な順南西を持ちます。ソースをスキーム(data:, https:)で指定することもできれば、ホスト名のみ指定(example.com、このホスト上の任意のオリジン・スキーム・ポートを許可)から完全修飾URIでの指定(https://example.com:443、HTTPSでexample.comでポート番号は443のみ許可)までの範囲で指定することもできます。ワイルドカードも利用できますが、スキーム、ポート、ホスト名の最後だけでしか利用できません。*://*.example.com:*はexample.comの全てのサブドメイン(ただしexample.com自身は不可)で、任意のスキーム、任意のポートにマッチします。

以下の4つのキーワードもソースリスト内で利用できます:

  • 'none' は、期待通り、何ともマッチしません。
  • 'self' は現在のオリジンにマッチしますが、サブドメインとはマッチしません。
  • 'unsafe-inline' はインラインのJavaScriptとCSSを許可します。(詳細については後ほど触れます)
  • 'unsafe-eval' はevalのような文字列をJavaScriptとして評価する機構を許可します。(これも後ほど)

これらのキーワードはシングルクォーテーションが必須です。script-src 'self' は現在のホストから取得したJavaScriptの実行を許可します。一方、script-src self は"self"という名前のサーバーから取得したJavaScriptを許可します(そして現在のホストからのものは許可しません)。これはおそらく期待する動作ではないでしょう。

サンドボックス

触れておく価値のあるディレクティブがもう一つ有ります。それがsandboxです。これはこれまで見てきたものとは少し異なり、ページがロード出来るリソースに対してというよりはページで実行出来るアクションを制限する場所になります。もしsandboxディレクティブが存在したら、そのページはsandbox属性を持つiframeの中にロードされたものとして扱われます。これはページに対して非常に広範な影響を与えます。そのページは独自のオリジンを持つようになり、フォームをサブミット出来ない、など。妥当なsandboxing属性の詳細についてはこの記事のスコープを少し外れますので、HTML5仕様書の"sandboxing flag set"セクションをみるといいでしょう。

インラインコードは有害だ

CSPは、ブラウザにリソースの特定の組をアクセス可能にさせそれ以外を禁止させる明確な指示の方法として、オリジンのホワイトリストに基礎をおいているということをはっきりとさせておくべきでしょう。しかし、オリジンに基づいたホワイトリストはXSS攻撃に寄って引き起こされる最も大きな脅威、inline script injection、の解決策にはなりません。攻撃者が不正なペイロードを含むスクリプトタグ(<script>sendMyDataToEvilDotCom()</script>)を注入できるとしたら、ブラウザは合法的なインラインスクリプトタグとそれらを区別する手段を持ちません。CSPはこの問題をインラインスクリプトを完全に禁止することで解決しました。明らかにこれが唯一の手段です。

これはスクリプトタグとして直接組み込まれるスクリプトだけでなく、インラインのイベントハンドラとjavascript: URLも合わせて禁止します。スクリプトタグの内容は外部ファイルに移し、javascript: URLと<a ... onclick="[JAVASCRIPT]"> は適切なaddEventListener呼出しで置き換える必要があります。例えば、次のようなコードは

<script>
  function doAmazingThings() {
    alert('YOU AM AMAZING!');
  }
</script>
<button onclick='doAmazingThings();'>Am I amazing?</button>

次のように書き換えることになるでしょう。

<!-- amazing.html -->
<script src='amazing.js'></script>
<button id='amazing'>Am I amazing?</button>
// amazing.js
function doAmazingThings() {
  alert('YOU AM AMAZING!');
}
document.addEventListener('DOMContentReady', function () {
  document.getElementById('amazing')
          .addEventListener('click', doAmazingThings);
});

書き換えられたコードにはCSPに沿っていること以上にたくさんの利点があります。CSPを使っているかどうかに関わらず、すでにこれはベストプラクティスです。インラインJavaScriptは構造や振る舞いを加える方法として決して採用するべきではありません。外部リソースの方がブラウザに取ってはキャッシュが容易で、開発者にとってもより理解しやすく、コンパイルやミニファイが有効に働きます。コードを外部リソースに移せば、それだけでよりよいコードになるでしょう。

インラインスタイルも同様の扱いです。CSSが可能にするさまざまな非常に巧妙なデータの抜き出しを防ぐために、style属性もstyleタグも外部スタイルシートに移すべきです。

もし本当にどうしてもインラインスクリプトやスタイルが必要なのであれば、script-srcやstyle-srcディレクティブの許可されたソースとして'unsafe-inline'を追加すれば実現できます。しかしなるべく避けましょう。インラインスクリプトを禁止することはCSPが提供するセキュリティ上最も大きな勝利なのです。インラインスタイルを禁止することも同様にあなたのアプリケーションを強固にするでしょう。全てのコードをインラインではなくして正しく動くと確認することは少し手間がかかります。しかしそれは実行する価値のあるトレードオフです。

EVALã‚‚

攻撃者がスクリプトを直接埋め込めないとしても、あなたのアプリケーションをなんとか騙してただの文字列を実行可能なJavaScriptに変換させ、望むように動作させることは可能かもしれません。eval()、new Function()、setTimeout([string], ...)、setInterval([string], ...)などは全て悪意のある何かを実行することになるかもしれない文字列が流し込まれる媒介に利用される可能性があります。このリスクに対するCSPのデフォルトの反応は、驚くまでもなく、これらの媒介物を全て完全にブロックします。

これはあなたがアプリケーションを作る上でちょっとしたインパクトがあるでしょう。

JSONは、evalに頼るのではなく、組み込みのJSON.parseを使ってパースしてください。ネィティブのJSON操作はIE8以降の全てのブラウザで利用可能で、完全に安全です。

現在使用している全てのsetTimeoutやsetIntervalは文字列ではなくインライン関数を利用するように変更してください。例えば

setTimeout("document.querySelector('a').style.display = 'none';", 10);

は次のように書き直すといいでしょう。

setTimeout(function () {
  document.querySelector('a').style.display = 'none';
}, 10);

実行時のインラインテンプレートは避けてください。多くのテンプレートライブラリは実行時にテンプレートを高速に生成するためにnew Function()を大量に使用しています。動的プログラミングを利用する洒落たアプリケーションかもしれませんが、悪意のある文字列を評価されるというリスクを招きます。いくつかのフレームワークはCSPをデフォルトでサポートしていて、evalを使用しない頑強なパーサーを代わりに利用できます。AngularJSのng-cspディレクティブはこのいい例の一つです。

しかし選択したテンプレート言語で(例えばHandlebarsのように)プリコンパイルが利用可能なら、それを利用したほうがいいかもしません。実行時変換をどんなに高速化したとしてもユーザー体験という観点ではプリコンパイルしたテンプレートにはかなわないでしょう。そして安全性もより高まります。完璧ですね。もしevalまたはその他の文字列-JavaScript変換の同類があなたのアプリケーションに取ってどうしようもなく本質的な部分を占めているのであれば、script-srcディレクティブの許可されたソースとして'unsafe-eval'を追加して利用を許可することができます。しかし、先程も書いたように、それはしないでください。文字列の実行を禁止することが、攻撃者があなたのサイトで許可されていないコードを実行することを困難にする一番の方法です。

レポート

信用できないリソースをクライアントサイドでブロックするというCSPの機能はユーザーにとって大勝利ですが、加えてサーバーになんらかの通知があれば、悪意のあるコードが埋め込まれるようなバグを見つけて退治でき非常に有益でしょう。このような目的で、JSON形式の違反レポートをreport-uriディレクティブで指定した場所にPOSTするようにブラウザに支持できます。

Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

このレポートは次のような形式になります。

{
  "csp-report": {
    "document-uri": "http://example.org/page.html",
    "referrer": "http://evil.example.com/",
    "blocked-uri": "http://evil.example.com/evil.js",
    "violated-directive": "script-src 'self' https://apis.google.com",
    "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
  }
}

ここには違反が発生したページ(document-uri)、ページのリファラ(注: referrerはミススペルではありません)、ページのポリシーに違反したリソース(blocked-uri)、違反されたディレクティブ(violated-directive)、ページのポリシー全体(original-policy)といった、違反の原因を追跡するのに十分な情報が含まれています。

レポートのみ

CSPを始めるに当たって、ユーザーに厳格なポリシーを適用する前に現在のアプリケーションの状態をきちんと把握しておきたいと感じるのは当然です。適用の足がかりとして、ブラウザにポリシーをモニタして違反をレポートはするけれど、制限は課さないように指示することができます。Content-Security-Policyヘッダを送る代わりにContent-Security-Policy-Report-Onlyヘッダを送ってください。

Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

report-onlyモードで指定されたポリシーは制限さてたリソースを一切ブロックしませんが、違反があれば指定した場所にレポートを送ります。両方のヘッダを送り、あるポリシーは実際に適用し、もう一方はモニタするだけということも可能です。これはアプリケーションのCSPを変更した場合の効果を測定するいい方法です。新しいポリシーのレポートを有効にして違反レポートを参考にバグを修正し、効果に納得できたら新しいポリシーの適用を開始すればいいのです。

実際の利用例

CSPはChrome 16以上とFirefox 4以上で利用可能で、IE 10で少なくとも限定的にはサポートされると期待されます。Safariは現在の実装には含まれていませんが、WebKit nightliesではChromeと同様に動作していて、Safariの次のイテレーションに含まれると期待できます。Twitterなどのような多くのサイトがこのヘッダをすでに発行していて(Twitterのケーススタディは読む価値があります)、この標準をあなたのサイトで採用する準備は十分に整っていると言えます。

自身のアプリケーションのためのポリシーを作成するための最初のステップは、実際にロードされているリソースを洗い出すことです。アプリのリソースがどのように関連しているか把握できたと感じたら、要求に従ってポリシーを策定します。CSPの保護制約を最も有効に働かせる方法を見つけるためにまずは標準的なユースケースを眺めてみましょう。

ユースケース #1: ソーシャルメディアウィジェット

  • Googleの+1ボタンはhttps://apis.google.comのスクリプトを含み、https://plusone.google.comのiframeを埋め込みます。ボタンを埋め込むにはこれら二つのオリジンをポリシーに含める必要があります。最小限のポリシーは次のようになるでしょう。script-src https://apis.google.com; frame-src https://plusone.google.com。さらにGoogleが提供するJavaScriptのスニペットは外部JavaScriptファイルに書かなければいけません。
  • FacebookのLikeボタンはたくさんの実装オプションがあります。おすすめは、あなたのサイトの他の部分を安全に保つためにiframeバージョンを使い続けることです。それを適切に機能させるためにはframe-src https://facebook.comディレクティブが必要です。デフォルトではFacebookが提供するiframeのコードは//facebook.comからの相対URLでロードされることに注意が必要です。明示的にHTTP:つまりhttps://facebook.comを指定するよう変更してください。必要のないところでHTTPを使用する理由はありません。
  • TwitterのTweetボタンは共にhttps://platform.twitter.com(先ほどと同様にTiwtterもデフォルトで相対URLを表示するので、ローカルにコピー&ペーストしたあとHTTPSを指定するよう書き換えてください)でホストされるスクリプトとフレームのアクセスに依存します。script-src https://platform.twitter.com; frame-src https://platform.twitter.comと指定し、Twitterが提供するJavaScriptスニペットを外部JavaScriptファイルに記述します。
  • その他のプラットフォームも同様な要求があり、同様に指定すればいいでしょう。まずdefault-srcは'none'に設定し、コンソールを見ながらウィジェットを動作させるのに必要なリソースを決定すればいいでしょう。

複数のウィジェットを使用するのも簡単です。単にポリシーディレクティブを一つにまとめてください。その際一つのタイプのリソースは一つのディレクティブに統合することを忘れないようにしましょう。もし上記の3つをすべて使用するのであればポリシーは次のようになります。

script-src https://apis.google.com https://platform.twitter.com; frame-src https://plusone.google.com https://facebook.com https://platform.twitter.com

ユースケース #2: 封鎖

今度は銀行のサイトを運営しているとして、自分で許可したリソースだけがロードできることを保証したいと考えたとします。このシナリオではデフォルトのポリシーとして全てを完璧にブロックする(default-src 'none')ところから開始します。

銀行は全ての画像、スタイル、スクリプトをhttps://cdn.mybank.netにあるCDNからロードし、様々なデータを取得するためにhttps://api.mybank.com/にXHRで接続するものとします。フレームは使用されますが、ローカルにあるページ(no third-party origins)だけを表示します。Flashもウェブフォントもなにも利用しません。このシナリオで送信することになるもっとも厳格なヘッダは次のようなものです。

Content-Security-Policy: default-src 'none'; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net; img-src https://cdn.mybank.net; connect-src https://api.mybank.com; frame-src 'self'

ユースケース #3: SSLのみ

結婚指輪のディスカッションフォーラムの管理者が全てのリソースはセキュアチャネル経由でのみロードされることを保証したいが、コードはほとんど書きたくないと考えているとします。インラインのスクリプトとスタイルで溢れているサードパーティ製のフォーラムソフトウェアの大部分を書き換えるというのは彼の能力を超えているのです。そうした場合、次のポリシーが効果的でしょう。

Content-Security-Policy: default-src https:; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'

default-srcでhttps:を指定したとしても、スクリプトとスタイルのディレクティブで自動的にそのソースが継承されるわけではありません。特定のリソースタイプのためのディレクティブはデフォルトを完全に上書きします。

将来

W3CのWeb Application Security Working GroupはContent Security Policyの使用の詳細について作業を続けていて、この記事で概要を説明した機能が含まれるバージョン1.0は最終勧告寸前です。しかしグループはその成果に安住しているわけではなくCSP 1.1について今もpublic-webappsec@メーリングリストで活発に議論されていて、ブラウザベンダーも実装を向上させるために懸命に作業しています。

CSP 1.1は構想段階ですがいくつかの面白い提案があり、そのうちいくつかはここで取り上げる価値があるでしょう:

  • metaタグを利用したポリシー注入: CSPはHTTPヘッダで配布するように想定されていますが、ポリシーをページに直接、もしくはスクリプトによってマークアップとして設定できれば常に有用でしょう。ポリシーが適用されるドキュメントと同じドキュメントにポリシーを書くべきかどうかについて健全な議論が行われていますが、十分確かなユースケースがあり次のイテレーションに組み込んでよさそうです。仕様のmeta要素に関する部分はWebKitで既に機能が実装されているので、Chromeで実際に試すことが出来ます。<meta http-equiv="X-WebKit-CSP" content="[POLICY GOES HERE]">をドキュメントのヘッダに書けばうまく動くでしょう。

    スクリプトでmetaタグを追加すれば実行時にポリシーを追加することもできます。必要な全てのリソースをロードし終わってから適切なポリシーを設定し、「ブートアップ」するのが完全に制限されたアプリケーションを実現するいい手段でしょう。これにより完全にセキュアなサイト(一時的に重大な攻撃を受ける可能性がある)を提供できませんが、HTTPヘッダを適用する前にCSPの恩恵をいくらかうけることができます。
  • DOM API: もしこの機能が次のイテレーションに入れば、ページの現在のポリシーをJavaScriptで取得できるようになります。それによって実行時に実装を決定したり、コードの環境に応じて正しく何かを設定することができます。例えばeval()が実行可能なら何らかの機能を異なる方法で実装できるかも知れません。これは特にフレームワークの作者にとって有用です。ただし、APIの仕様はまだ非常に不安定なので、最新のイテレーションについて知るにはドラフト仕様の「Script Interfaces」の節を参照してください。
  • 新しいディレクティブ: 様々な新しいディレクティブが議論されています。script-nonceは明示的に指定された要素についてだけインラインスクリプトを実行できます。plugin-typesはプラグインがロードできるコンテンツのMIMEタイプを制限します。form-actionは特定のオリジンにだけformをサブミットできます。それ以外にもありますが、現在のところ仕様があいまいです。

これらの新しい機能に関する議論に興味があれば、メーリングリストを眺めるか、実際に参加してください。