Content Security PolicyとCMP:CSPと同意を両立させる
足並みをそろえるべき2つのセキュリティ層
Content Security Policy(CSP)は、ページがスクリプト、スタイル、画像、フレーム、ネットワークリクエストをどのソースから読み込んでよいかをブラウザに伝えます。同意管理プラットフォームはその逆の役割を果たし、ユーザーの選択に基づいて、それらのスクリプトのうちどれを実行してよいかを決定します。両者を別々に設定すると、衝突します。CSPにブロックされてCMPが読み込めなくなるか、CMPや広告タグに対応するためにCSPを緩めすぎて、もう何も保護しなくなってしまうのです。
このガイドでは、CMPが通常必要とするCSPディレクティブ、nonceがスクリプトのインジェクションとどう関わるか、そしてバナーを壊さずに変更を導入する方法を説明します。
CMPがCSPに通常求めるもの
正確な要件はベンダーによって異なるため、必ずCMP自身のドキュメントから始めてください。一般的には、次のディレクティブを確認します。
script-src— CMPスクリプトの読み込み元ドメインと、CMPが読み込む追加のスクリプトファイル。connect-src— CMPがfetchまたはXHRで呼び出すエンドポイント(設定、ベンダーリスト、位置情報、同意ログのエンドポイントなど)。style-src— スタイルシートのドメインと、CMPが<style>要素を挿入したりstyle属性を設定したりする場合はインラインスタイルも。img-src— CMPが使うロゴ、アイコン、トラッキングを行わないピクセル。font-src— バナーが独自のフォントを読み込む場合。frame-src— CMPがiframeを使う場合(たとえばドメイン間の同意共有)。
TCFのサイトでは、CMPがページ上に__tcfapiLocatorという名前のロケータフレームも作成することを覚えておいてください。このフレームはリモートのソースなしで作成されるため、通常はframe-srcのエントリは不要ですが、テストで確認する価値があります。
インラインスタブの問題
ほとんどのCMPは、メインのCMPが読み込まれる前に他のスクリプトが呼び出しをキューに入れられるよう、小さなインラインスクリプト(スタブ)をページのheadに置くよう求めます。厳格なCSPは、明示的に許可しない限りインラインスクリプトをブロックします。選択肢は3つあります。
- nonce。レスポンスごとにランダムな値を生成し、CSPヘッダーに
'nonce-...'として、またスタブの<script>タグに追加します。動的なページに最も柔軟な選択肢です。 - ハッシュ。スタブの内容そのもののSHA-256ハッシュを
script-srcに追加します。静的サイトでうまく機能しますが、スタブを少しでも、空白ひとつでも変更すると機能しなくなります。 - スタブのセルフホスティング。自分のドメイン上の外部ファイルとして配置し、
'self'でカバーします。十分早く読み込まれるよう注意が必要です。
近道として'unsafe-inline'を使うのは避けてください。すべてのインラインスクリプトを許可してしまい、CSPが提供する保護の多くが失われます。
nonceとCMPがインジェクションするスクリプト
同意があるまでスクリプトをブロックするCMPは、多くの場合、スクリプトを保留し、後からインジェクションすることで動作します。nonceベースのCSPでは、それらのインジェクションされたスクリプトも許可される必要があります。一般的なアプローチは2つあります。
- nonceの伝播。CMPが自身のスクリプトタグからnonceを読み取り、インジェクションする各スクリプトに追加します。お使いのCMPが対応しているか確認してください。
'strict-dynamic'。このキーワードは、対応するブラウザに、すでに信頼されたスクリプトによって読み込まれたスクリプトを信頼するよう指示します。スクリプトをインジェクションするタグマネージャーやCMPとの運用が楽になりますが、信頼したスクリプトが他のスクリプトを読み込めるようになることも意味するため、慎重に信頼してください。
タグマネージャーでも同じ問題が生じます。広告やアナリティクスのタグをタグマネージャー経由で読み込む場合は、nonceに関するそのドキュメントに従い、CMPとタグマネージャーが一貫したアプローチを使うようにしてください。
広告タグがCSPを難しくする
プログラマティック広告は多数のドメインからスクリプトとフレームを読み込み、そのリストは需要パートナーの変化に伴って変わります。パブリッシャーは通常、次のどちらかを選びます。
- プログラマティック広告のないページでは厳格なCSPを、広告が動作するページではより広いポリシーを使う。
frame-ancestors、object-src 'none'、base-uriなど、管理可能なディレクティブに焦点を当てたポリシーとし、スクリプトソースは広めにしておく。
どちらの場合でも、CMPはワイルドカードだけでカバーされるのではなく、明示的な許可リストに入れておくべきです。そうすれば、CSPの変更がバナーを気づかないうちに無効にすることはありません。
Report-Onlyモードで導入する
まずContent-Security-Policy-Report-Onlyヘッダーを使います。ブラウザは何もブロックせずに違反を報告するため、新しいポリシーが何を壊すかを確認できます。
- レポートエンドポイントを指定して、ポリシーをreport-onlyモードでデプロイします。
- クリーンなブラウザプロファイルでページを読み込み、初回訪問、すべて承諾、すべて拒否、カスタム選択、撤回という同意フローの全体を通して確認します。
- 違反レポートとブラウザコンソールで、ブロックされたCMPリソースがないか確認します。
- ポリシーを調整し、その後で強制モードに切り替えます。
- 強制後もレポートを有効にしておき、退行を検知します。
CSPの競合の症状
- バナーがまったく表示されず、コンソールにCMPスクリプトのCSP違反が表示される。
- バナーは表示されるがスタイルが当たっていない。
style-srcがそのCSSをブロックしているため。 - 選択が保存されない。
connect-srcが同意ログの呼び出しをブロックしているため。 - 同意後に広告やアナリティクスが読み込まれない。インジェクションされたスクリプトにnonceがないため。
__tcfapiが未定義。インラインスタブがブロックされたため。
重要なポイント
- CMPについて、
script-src、connect-src、style-src、img-src、frame-srcを確認しましょう。 - インラインスタブは
'unsafe-inline'ではなく、nonceまたはハッシュで許可します。 - 同意後にインジェクションされるスクリプトが、nonceを受け取るか
'strict-dynamic'でカバーされるようにします。 - 強制する前に、すべての同意パスをreport-onlyモードでテストします。
- CSPの変更でCMPが無効にならないよう、CMPを明示的に許可リストに入れておきます。