ウェブアプリに悪意のあるスクリプトを挿入するクロスサイト スクリプティング(XSS)は、この 10 年以上にわたって最大のウェブ セキュリティの脆弱性の一つです。
コンテンツ セキュリティ ポリシー(CSP)は、XSS の軽減に役立つ追加のセキュリティ レイヤです。CSP を構成するには、ウェブページに Content-Security-Policy
HTTP ヘッダーを追加し、ユーザー エージェントがそのページで読み込めるリソースを制御する値を設定します。
このページでは、一般的に使用されているホスト許可リストベースの CSP ではなく、ノンスまたはハッシュに基づく CSP を使用する方法について説明します。これらの CSP は、ほとんどの構成でバイパスできるため、ページが XSS にさらされることがよくあります。
重要な用語: ノンスは、<script>
タグを信頼できるものとしてマークするために使用できる、1 回だけ使用される乱数です。
重要な用語: ハッシュ関数とは、入力値をハッシュと呼ばれる圧縮数値に変換する数学関数です。ハッシュ(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
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>
タグを示します。 - これは、信頼できるスクリプトが作成するスクリプトの実行を自動的に許可することで、ノンスまたはハッシュベースの CSP をデプロイする労力を削減するように
'strict-dynamic'
を設定します。これにより、ほとんどのサードパーティの JavaScript ライブラリやウィジェットを使用できなくなります。 - URL 許可リストに基づいていないため、一般的な CSP のバイパスの影響を受けません。
- インライン イベント ハンドラや
javascript:
URI などの信頼できないインライン スクリプトはブロックされます。 object-src
を制限して、Flash などの危険なプラグインを無効にします。base-uri
を制限して、<base>
タグの挿入をブロックします。これにより、相対 URL から読み込まれたスクリプトの場所が攻撃者によって変更されるのを防ぐことができます。
厳格な CSP を導入する
厳格な CSP を導入するには、以下を行う必要があります。
- アプリケーションでノンスベースの CSP とハッシュベースの CSP のどちらを設定するかを決定します。
- [厳格な CSP 構造] セクションから CSP をコピーし、アプリケーション全体でレスポンス ヘッダーとして設定します。
- HTML テンプレートとクライアント側コードをリファクタリングして、CSP と互換性のないパターンを削除します。
- CSP をデプロイします。
このプロセス全体を通して、Lighthouse
(v7.3.0 以降とフラグ --preset=experimental
)ベスト プラクティスの監査を使用して、サイトが CSP に準拠しているか、サイトに CSP に対する準拠が十分にあるか、XSS に十分準拠しているかどうかを確認できます。
ステップ 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>
タグ。ローカルで開発する場合は、CSP を調整してサイトに与える影響をすばやく確認する場合は、<meta>
タグのほうが便利です。ただし、次の点に注意してください。- 後で 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 nonce は次の条件を満たす必要があります。
- 暗号的に強いランダム値(理想的には 128 ビット以上)
- すべての回答に対して新しく生成
- Base64 エンコード
以下に、サーバーサイド フレームワークに CSP ノンスを追加する方法の例を示します。
- Django(python)
- Express(JavaScript):
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>
<script src="https://example.org/foo.js"></script> <script src="https://example.org/bar.js"></script>
スクリプトの読み込みに関する考慮事項
インライン スクリプトの例では、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 違反が表示されます。
ほとんどの場合、修正は簡単です。
インライン イベント ハンドラのリファクタリング
<span id="things">A thing.</span> <script nonce="${nonce}"> document.getElementById('things').addEventListener('click', doThings); </script>
<span onclick="doThings();">A thing.</span>
javascript:
URI のリファクタリング
<a id="foo">foo</a> <script nonce="${nonce}"> document.getElementById('foo').addEventListener('click', linkClicked); </script>
<a href="javascript:linkClicked()">foo</a>
JavaScript から eval()
を削除する
アプリケーションで eval()
を使用して JSON 文字列のシリアル化を JS オブジェクトに変換する場合、そのようなインスタンスを JSON.parse()
にリファクタリングすると、やはり高速になります。
eval()
の使用をすべて削除できない場合でも、厳格なノンスベースの CSP を設定できますが、'unsafe-eval'
CSP キーワードを使用する必要があります。これにより、ポリシーの安全性が若干低下します。
このようなリファクタリングの例については、次の CSP の厳格な Codelab をご覧ください。
ステップ 4(省略可): 古いバージョンのブラウザをサポートする代替手段を追加する
古いバージョンのブラウザをサポートする必要がある場合:
strict-dynamic
を使用するには、以前のバージョンの Safari の代替としてhttps:
を追加する必要があります。その場合、次の処理が行われます。strict-dynamic
をサポートするすべてのブラウザはhttps:
フォールバックを無視するため、ポリシーの強度が低下することはありません。- 古いブラウザでは、外部ソースのスクリプトは HTTPS オリジンからのみ読み込むことができます。これは厳格な CSP よりも安全性が低くなりますが、
javascript:
URI のインジェクションなどの一般的な XSS の原因は防止されます。
- 非常に古いバージョン(4 年以上)のブラウザとの互換性を確保するために、
unsafe-inline
をフォールバックとして追加できます。CSP nonce または hash が存在する場合、最近のブラウザはすべてunsafe-inline
を無視します。
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
ステップ 5: CSP をデプロイする
CSP がローカル開発環境の正規のスクリプトをブロックしていないことを確認したら、CSP をステージング環境にデプロイしてから、本番環境にデプロイできます。
- (省略可)
Content-Security-Policy-Report-Only
ヘッダーを使用して、レポート専用モードで CSP をデプロイします。レポート専用モードは、CSP の制限の適用を開始する前に、本番環境で新しい CSP のような破壊的変更をテストする場合に便利です。レポート専用モードでは、CSP はアプリの動作に影響を与えませんが、ブラウザは CSP と互換性のないパターンを検出した場合もコンソール エラーと違反レポートを生成するため、エンドユーザーにとって何が壊れているかを確認できます。詳しくは、Reporting API をご覧ください。 - CSP によってエンドユーザーのサイトが破損しないことがわかっている場合は、
Content-Security-Policy
レスポンス ヘッダーを使用して CSP をデプロイします。CSP は、<meta>
タグよりも安全であるため、HTTP ヘッダーサーバーサイドを使用して設定することをおすすめします。この手順が完了すると、CSP は XSS からアプリの保護を開始します。
制限事項
厳格な CSP は通常、XSS の軽減に役立つ強力なセキュリティ レイヤを提供します。ほとんどの場合、CSP は javascript:
URI などの危険なパターンを拒否することで、攻撃対象領域を大幅に縮小します。ただし、使用している CSP の種類(ノンス、ハッシュ、'strict-dynamic'
の有無)によっては、CSP がアプリを保護しない場合もあります。
- スクリプトをノンスしているが、本文または
<script>
要素のsrc
パラメータに直接インジェクションがある場合。 - 動的に作成されたスクリプト(
document.createElement('script')
)の場所へのインジェクション(引数の値に基づいてscript
DOM ノードを作成するライブラリ関数など)がある場合。これには、jQuery の.html()
や、3.0 より前の jQuery の.get()
および.post()
などの一般的な API が含まれます。 - 古い AngularJS アプリケーションにテンプレート インジェクションがある場合。AngularJS のテンプレートに挿入できる攻撃者は、これを使用して任意の JavaScript を実行できます。
- ポリシーに
'unsafe-eval'
が含まれている場合、eval()
、setTimeout()
、およびあまり使用されない他のいくつかの API へのインジェクション。
デベロッパーとセキュリティ エンジニアは、コードレビューやセキュリティ監査の際に、このようなパターンに特に注意を払う必要があります。これらのケースについて詳しくは、コンテンツ セキュリティ ポリシー: 強化と緩和の効果的な方法をご覧ください。