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

対応ブラウザ

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

ソース

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

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

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

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

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

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

厳格な CSP を使用する理由

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

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

厳格な CSP 構造

基本的な厳格な Content Security Policy では、次のいずれかの 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 とハッシュベースの CSP のどちらを設定するかを決定します。
  2. [厳格な CSP 構造] セクションから CSP をコピーし、アプリ全体でレスポンス ヘッダーとして設定します。
  3. HTML テンプレートとクライアントサイド コードをリファクタリングして、CSP と互換性のないパターンを削除します。
  4. CSP をデプロイします。

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

Lighthouse レポートに、適用モードで CSP が検出されないという警告が表示される。
サイトに CSP がない場合、Lighthouse にはこの警告が表示されます。

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

厳格な CSP の 2 つのタイプは次のとおりです。

ノンスベースの 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 メタタグはレポート専用モードをサポートしていないためです。

アプリケーションで次の 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 で許可されるように、これらの属性をすべてのスクリプトに追加することです。

アプリケーションで次の 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>
これらのスクリプトは動的に追加されておらず、許可されたソースに一致する integrity 属性がないため、CSP によってブロックされます。

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

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

  • ドキュメントのダウンロードが完了する前に、1 つまたは両方のスクリプトが実行される可能性があります。スクリプトの実行時にドキュメントを準備する場合は、スクリプトを追加する前に 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(省略可): 古いブラウザ バージョンをサポートするフォールバック機能を追加する

対応ブラウザ

  • 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 をデプロイします。<meta> タグよりも安全であるため、サーバーサイドで HTTP ヘッダーを使用して CSP を設定することをおすすめします。この手順が完了すると、CSP が XSS からアプリを保護するようになります。

制限事項

厳格な CSP は通常、XSS を軽減する強力なセキュリティ レイヤを追加します。ほとんどの場合、CSP は javascript: URI などの危険なパターンを拒否することで、攻撃対象領域を大幅に減らします。ただし、使用している CSP のタイプ(nonce、hash、'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 への挿入。

デベロッパーとセキュリティ エンジニアは、コードレビューとセキュリティ監査中にこのようなパターンに特に注意する必要があります。これらのケースの詳細については、Content Security Policy: A Successful Mess Between Hardening and Mitigation をご覧ください。

関連情報