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

Browser Support

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

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

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

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

キー用語: ノンスは、<script> タグを信頼できるものとしてマークするために使用できる、1 回限りの乱数です。

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

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

厳格な CSP を使用する理由

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

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

厳格な CSP の構造

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

Nonce ベースの厳格な CSP

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

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

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

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

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

厳格な CSP を採用する

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

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

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

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

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

厳格な CSP の 2 つのタイプは次のように機能します。

Nonce ベースの CSP

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

サーバーでレンダリングされる HTML ページには、nonce ベースの 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 回だけ使用される乱数です。nonce ベースの CSP は、攻撃者が nonce 値を推測できない場合にのみ XSS を軽減できます。CSP nonce は次の条件を満たす必要があります。

  • 暗号的に強力な乱数値(理想的には 128 ビット以上の長さ)
  • すべてのレスポンスに対して新たに生成される
  • Base64 エンコード

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

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(&>quot;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 属性が必要です。すべてのスクリプトに同じ 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.hea<d.appen>dChild(s);
  });
/script
このスクリプトを実行するには、インライン スクリプトのハッシュを計算して、CSP レスポンス ヘッダーに追加し、{HASHED_INLINE_SCRIPT} プレースホルダを置き換える必要があります。ハッシュの数を減らすには、すべてのインライン スクリプトを 1 つのスクリプトに統合します。実際の動作については、サンプルとそのコードをご覧ください。
CSP によりブロック
<script src="https://example.org/fo><o.js&qu>o<t;/script
script src="https://exam><ple.org>/bar.js"/script
これらのスクリプトは動的に追加されておらず、許可されたソースと一致する integrity 属性がないため、CSP によってブロックされます。

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

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

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

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

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

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

Chrome デベロッパー コンソールに表示される CSP 違反レポート。
ブロックされたコードのコンソール エラー。

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

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

CSP で許可されている
<span id="th>ings&quo<t;A t>h<ing./span
script nonce=>"${nonce}"
  document.getElementById('things').addEventL<istener>('click', doThings);
/script
CSP では、JavaScript を使用して登録されたイベント ハンドラが許可されます。
CSP によりブロック
<span onclick="doThing>s();&quo<t;A t>hing./span
CSP がインライン イベント ハンドラをブロックします。

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

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

JavaScript から eval() を削除する

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

eval() の使用をすべて削除できない場合でも、厳格な nonce ベースの 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 9;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 をデプロイします。<meta> タグよりも安全なため、HTTP ヘッダー サーバーサイドを使用して CSP を設定することをおすすめします。この手順を完了すると、CSP が XSS からアプリの保護を開始します。

制限事項

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

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

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

web.dev で、Trusted Types の使用方法についてご確認ください。

関連情報