厳格なコンテンツ セキュリティ ポリシー(CSP)を使用してクロスサイト スクリプティング(XSS)を軽減する

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

クロスサイト スクリプティング(XSS)は、ウェブアプリに悪意のあるスクリプトを挿入する機能であり、10 年以上にわたりウェブ セキュリティの最大の脆弱性の 1 つとなっています。

コンテンツ セキュリティ ポリシー(CSP)は、XSS の軽減に役立つセキュリティ レイヤです。CSP を構成するには、Content-Security-Policy HTTP ヘッダーをウェブページに追加し、ユーザー エージェントがそのページに読み込むことができるリソースを制御する値を設定します。

このページでは、ノンスまたはハッシュに基づく CSP を使用して XSS を軽減する方法について説明します。一般的に使用されているホスト許可リストに基づく CSP は、ほとんどの構成でバイパスできるため、ページが XSS にさらされることがよくあります。

重要な用語: ノンスは、1 回だけ使用される乱数で、<script> タグを信頼できるものとしてマークするために使用できます。

重要な用語: ハッシュ関数は、入力値をハッシュと呼ばれる圧縮された数値に変換する数学関数です。ハッシュ(SHA-256 など)を使用して、インライン <script> タグを信頼できるものとしてマークできます。

ノンスまたはハッシュに基づくコンテンツ セキュリティ ポリシーは、多くの場合、厳格な CSP と呼ばれます。アプリケーションで厳格な CSP を使用している場合、HTML インジェクションの欠陥を見つけた攻撃者は、通常、その欠陥を使用して、脆弱なドキュメントで悪意のあるスクリプトをブラウザに強制的に実行させることはできません。これは、厳格な CSP では、ハッシュ化されたスクリプト、またはサーバーで生成された正しいノンス値を持つスクリプトのみが許可されるためです。攻撃者は、特定のレスポンスの正しいノンスを知らないとスクリプトを実行できません。

厳格な CSP を使用する理由

サイトにすでに script-src www.googleapis.com のような CSP がある場合、クロスサイトに対しては効果がない可能性があります。このタイプの CSP は、許可リスト CSP と呼ばれます。多くのカスタマイズが必要であり、攻撃者によってバイパスされる可能性があります。

暗号化ノンスまたはハッシュに基づく厳格な CSP は、これらの落とし穴を回避します。

厳格な CSP 構造

基本的な厳格なコンテンツ セキュリティ ポリシーでは、次のいずれかの HTTP レスポンス ヘッダーを使用します。

ノンスベースの厳格な CSP

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
ノンスベースの厳格な CSP の仕組み。

ハッシュベースの厳格な CSP

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

次のようなプロパティにより、CSP は「厳格」になり、安全になります。

  • ノンス 'nonce-{RANDOM}' またはハッシュ 'sha256-{HASHED_INLINE_SCRIPT}' を使用して、サイトのデベロッパーがユーザーのブラウザで実行することを信頼している <script> タグを示します。
  • 'strict-dynamic' を設定すると、信頼できるスクリプトが作成するスクリプトの実行が自動的に許可されるため、ノンスベースまたはハッシュベースの CSP をデプロイする手間を軽減できます。これにより、ほとんどのサードパーティの JavaScript ライブラリとウィジェットの使用もブロック解除されます。
  • URL 許可リストに基づいていないため、一般的な CSP バイパスの影響を受けません。
  • インライン イベント ハンドラや javascript: URI などの信頼できないインライン スクリプトをブロックします。
  • object-src を制限して、Flash などの危険なプラグインを無効にします。
  • base-uri を制限して、<base> タグの挿入をブロックします。これにより、攻撃者が相対 URL から読み込まれたスクリプトの場所を変更できなくなります。

厳格な CSP を採用する

厳格な CSP を採用するには、次のことを行う必要があります。

  1. アプリケーションでノンスベースまたはハッシュベースの CSP を設定する必要があるかどうかを判断します。
  2. 厳格な CSP 構造セクションから CSP をコピーし、アプリケーション全体でレスポンス ヘッダーとして設定します。
  3. HTML テンプレートとクライアントサイド コードをリファクタリングして、CSP と互換性のないパターンを削除します。
  4. CSP をデプロイします。

このプロセス全体で Lighthouse(v7.3.0 以降、フラグ --preset=experimental を指定)のベスト プラクティス監査を使用して、サイトに CSP があるかどうか、また XSS に対して効果的なほど厳格かどうかを確認できます。

Lighthouse レポートの警告。自動適用モードで CSP が見つからないことを示しています。
サイトに CSP がない場合、Lighthouse はこの警告を表示します。

ステップ 1: ノンスベースまたはハッシュベースの CSP が必要かどうかを判断する

2 種類の厳格な CSP の仕組みは次のとおりです。

ノンスベースの CSP

ノンスベースの CSP では、実行時に乱数を生成し、それを CSP に含めて、ページ内のすべてのスクリプトタグに関連付けます。攻撃者は、そのスクリプトの正しい乱数を推測する必要があるため、ページに悪意のあるスクリプトを含めたり、実行したりすることはできません。この方法は、番号が推測不可能で、レスポンスごとに実行時に新しく生成される場合にのみ有効です。

サーバーでレンダリングされる HTML ページには、ノンスベースの CSP を使用します。これらのページでは、レスポンスごとに新しい乱数を作成できます。

ハッシュベースの CSP

ハッシュベースの CSP の場合、すべてのインライン スクリプトタグのハッシュが CSP に追加されます。各スクリプトには異なるハッシュがあります。攻撃者は、ページに悪意のあるスクリプトを含めたり、実行したりすることはできません。スクリプトを実行するには、そのスクリプトのハッシュが CSP に含まれている必要があるためです。

静的に提供される HTML ページや、キャッシュに保存する必要があるページには、ハッシュベースの CSP を使用します。たとえば、Angular や React などのフレームワークで構築された、サーバーサイド レンダリングなしで静的に提供されるシングルページ ウェブ アプリケーションには、ハッシュベースの CSP を使用できます。

ステップ 2: 厳格な CSP を設定してスクリプトを準備する

CSP を設定する際には、いくつかのオプションがあります。

  • レポート専用モード(Content-Security-Policy-Report-Only)または自動適用モード(Content-Security-Policy)。レポート専用モードでは、CSP はリソースをブロックしないため、サイト上のリソースが破損することはありませんが、ブロックされた可能性のあるエラーを確認してレポートを取得できます。ローカルで CSP を設定する場合、どちらのモードでもブラウザ コンソールにエラーが表示されるため、これはあまり重要ではありません。リソースをブロックするとページが破損しているように見えることがあるため、自動適用モードは、ドラフト CSP がブロックするリソースを見つけるのに役立ちます。レポート専用モードは、プロセスの後半で最も役立ちます(ステップ 5 を参照)。
  • ヘッダーまたは HTML の <meta> タグ。ローカル開発では、<meta> タグを使用すると、CSP を調整してサイトへの影響をすばやく確認できるため、より便利です。ただし、次のようにする必要があります。
    • 後で CSP を本番環境にデプロイするときは、HTTP ヘッダーとして設定することをおすすめします。
    • CSP をレポート専用モードで設定する場合は、ヘッダーとして設定する必要があります。CSP メタタグはレポート専用モードをサポートしていないためです。

オプション A: ノンスベースの CSP

アプリケーションで次の Content-Security-Policy HTTP レスポンス ヘッダーを設定します。

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

CSP 用のノンスを生成する

ノンスは、ページ読み込みごとに 1 回だけ使用される乱数です。ノンスベースの CSP は、攻撃者がノンス値を推測できない場合にのみ XSS を軽減できます。CSP ノンスは次の条件を満たす必要があります。

  • 暗号的に強力なランダム値(理想的には 128 ビット以上の長さ)
  • レスポンスごとに新しく生成される
  • Base64 エンコード

サーバーサイド フレームワークで CSP ノンスを追加する方法の例をいくつか紹介します。

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

<script> 要素に nonce 属性を追加する

ノンスベースの CSP では、すべての <script> 要素に、CSP ヘッダーで指定されたランダムなノンス値と一致する nonce 属性が必要です。すべてのスクリプトに同じノンスを使用できます。最初のステップは、CSP が許可するように、これらの属性をすべてのスクリプトに追加することです。

オプション B: ハッシュベースの CSP レスポンス ヘッダー

アプリケーションで次の Content-Security-Policy HTTP レスポンス ヘッダーを設定します。

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

複数のインライン スクリプトの場合、構文は 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}' です。

ソース スクリプトを動的に読み込む

インライン スクリプトを使用して、サードパーティ スクリプトを動的に読み込むことができます。

スクリプトをインライン化する方法の例。
CSP で許可
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
このスクリプトを実行するには、インライン スクリプトのハッシュを計算し、{HASHED_INLINE_SCRIPT} プレースホルダを置き換えて CSP レスポンス ヘッダーに追加する必要があります。ハッシュの数を減らすには、すべてのインライン スクリプトを 1 つのスクリプトに統合します。実際の動作については、こちらの例とそのコードをご覧ください。
CSP によりブロック
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
CSP は、これらのスクリプトが動的に追加されておらず、許可されたソースに一致する integrity 属性がないため、これらのスクリプトをブロックします。

スクリプトの読み込みに関する考慮事項

インライン スクリプトの例では、s.async = false を追加して、bar が最初に読み込まれた場合でも、foo が bar の前に実行されるようにしています。このスニペットでは、スクリプトが動的に追加されるため、スクリプトの読み込み中に s.async = false がパーサーをブロックすることはありません。パーサーは、async スクリプトの場合と同様に、スクリプトの実行中にのみ停止します。ただし、このスニペットを使用する場合は、次の点に注意してください。

  • ドキュメントのダウンロードが完了する前に、一方または両方のスクリプトが実行される可能性があります。スクリプトの実行時にドキュメントを準備しておく場合は、スクリプトを追加する前に DOMContentLoaded イベントを待ちます。スクリプトのダウンロードが十分に早く開始されないためにパフォーマンスの問題が発生する場合は、ページの早い段階でプリロードタグを使用します。
  • defer = true は何もしません。この動作が必要な場合は、必要に応じてスクリプトを手動で実行します。

ステップ 3: HTML テンプレートとクライアントサイド コードをリファクタリングする

インライン イベント ハンドラ(onclick="…"、onerror="…" など)と JavaScript URI(<a href="javascript:…">)を使用して、スクリプトを実行できます。つまり、XSS バグを見つけた攻撃者は、この種の HTML を挿入して悪意のある JavaScript を実行できます。ノンスベースまたはハッシュベースの CSP では、このようなマークアップの使用が禁止されています。サイトでこれらのパターンを使用している場合は、より安全な代替パターンにリファクタリングする必要があります。

前のステップで CSP を有効にした場合、CSP が互換性のないパターンをブロックするたびに、コンソールに CSP 違反が表示されます。

Chrome デベロッパー コンソールの CSP 違反レポート。
ブロックコードのコンソール エラー。

ほとんどの場合、修正は簡単です。

インライン イベント ハンドラをリファクタリングする

CSP で許可
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
CSP では、JavaScript を使用して登録されたイベント ハンドラが許可されます。
CSP によりブロック
<span onclick="doThings();">A thing.</span>
CSP はインライン イベント ハンドラをブロックします。

javascript: URI をリファクタリングする

CSP で許可
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
CSP では、JavaScript を使用して登録されたイベント ハンドラが許可されます。
CSP によりブロック
<a href="javascript:linkClicked()">foo</a>
CSP は javascript: URI をブロックします。

JavaScript から eval() を削除する

アプリケーションで eval() を使用して JSON 文字列シリアル化を JS オブジェクトに変換している場合は、そのようなインスタンスを JSON.parse() にリファクタリングする必要があります。これは高速でもあります。

eval() のすべての使用を削除できない場合でも、厳格なノンスベースの CSP を設定できますが、'unsafe-eval' CSP キーワードを使用する必要があります。これにより、ポリシーのセキュリティが若干低下します。

このようなリファクタリングの例については、こちらの厳格な CSP Codelab をご覧ください。

ステップ 4(省略可): 古いブラウザ バージョンをサポートするための代替手段を追加する

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

古いブラウザ バージョンをサポートする必要がある場合:

  • strict-dynamic を使用する場合は、以前のバージョンの Safari の代替として https: を追加する必要があります。具体的には次のとおりです。
    • strict-dynamic をサポートするすべてのブラウザは https: フォールバックを無視するため、ポリシーの強度が低下することはありません。
    • 古いブラウザでは、外部ソースのスクリプトは HTTPS オリジンから提供されている場合にのみ読み込むことができます。これは厳格な CSP よりも安全性が低くなりますが、javascript: URI の挿入など、一般的な XSS の原因を防ぐことができます。
  • 非常に古いブラウザ バージョン(4 年以上前)との互換性を確保するには、フォールバックとして unsafe-inline を追加します。CSP ノンスまたはハッシュが存在する場合、最近のブラウザはすべて unsafe-inline を無視します。
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

ステップ 5: CSP をデプロイする

CSP がローカル開発環境で正当なスクリプトをブロックしていないことを確認したら、CSP をステージング環境にデプロイし、その後本番環境にデプロイします。

  1. (省略可)Content-Security-Policy-Report-Only ヘッダーを使用して、レポート専用モードで CSP をデプロイします。レポート専用モードは、CSP 制限の適用を開始する前に、本番環境で新しい CSP などの互換性を破る可能性のある変更をテストするのに便利です。レポート専用モードでは、CSP はアプリの動作に影響しませんが、CSP と互換性のないパターンが検出されると、ブラウザはコンソール エラーと違反レポートを生成します。これにより、エンドユーザーに影響する可能性のある問題を把握できます。詳細については、Reporting API をご覧ください。
  2. CSP がエンドユーザーのサイトを破損しないことが確実な場合は、Content-Security-Policy レスポンス ヘッダーを使用して CSP をデプロイします。CSP は、<meta> タグよりも安全な HTTP ヘッダー サーバーサイドを使用して設定することをおすすめします。このステップを完了すると、CSP が XSS からアプリを保護し始めます。

制限事項

厳格な CSP は、一般的に XSS の軽減に役立つ強力なセキュリティ レイヤを追加します。ほとんどの場合、CSP は javascript: URI などの危険なパターンを拒否することで、攻撃対象領域を大幅に縮小します。ただし、使用している CSP のタイプ(nonce、ハッシュ、'strict-dynamic' の有無)によっては、CSP がアプリを十分に保護できない場合があります。

  • スクリプトにノンスを付加したが、その <script> 要素の本文または src パラメータに直接インジェクションがある場合。
  • 動的に作成されたスクリプト(document.createElement('script'))の場所へのインジェクションがある場合。これには、引数の値に基づいて script DOM ノードを作成するライブラリ関数へのインジェクションも含まれます。これには、jQuery の .html() などの一般的な API や、jQuery 3.0 未満の .get() と .post() が含まれます。
  • 古い AngularJS アプリケーションにテンプレート インジェクションがある場合。AngularJS テンプレートに挿入できる攻撃者は、これを使用して任意の JavaScript を実行できます。
  • ポリシーに 'unsafe-eval' が含まれている場合、eval()、setTimeout()、およびその他のあまり使用されない API へのインジェクション。

デベロッパーとセキュリティ エンジニアは、コードレビューとセキュリティ監査中に、このようなパターンに特に注意を払う必要があります。これらのケースについて詳しくは、コンテンツ セキュリティ ポリシー: 強化と緩和の間の成功した混乱をご覧ください。

関連情報