ブログに戻る

アナリティクススクリプトは Content-Security-Policy の穴になっている

厳格な CSP は XSS を防ぐ。だがサードパーティのアナリティクスタグがその穴を再び開ける。ホスト許可リストや SRI の欠如がポリシーを台無しにする理由と、ファーストパーティトラッカーが解決する点を解説する。

Content-Security-Policy は、クロスサイトスクリプティングに対する最も効果的な単一の防御策だ。だがサードパーティのアナリティクスタグを追加した瞬間、たいていそこに穴を開けざるを得なくなる。その穴こそ、現実世界の CSP バイパスの大半が潜んでいる場所だ。

その理由は偶然ではなく構造的なものだ。監視色の強いアナリティクススクリプトは、あなたが制御できないドメインから、実行時にさらにコードを読み込むよう設計されている。厳格な CSP は、まさにそれを禁じるために存在している。

ホスト許可リストはスクリプトを信頼する弱い方法

アナリティクスベンダーを許可する古いやり方が、ホスト許可リストだ。

Content-Security-Policy: script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;

OWASP はもはやこのパターンを推奨していない。許可リストは簡単にバイパスできる。許可されたオリジンが JSONP エンドポイント、オープンリダイレクト、あるいはユーザーがアップロードしたスクリプトをホストしていれば、それは XSS のベクターになる。タグマネージャーは事態をさらに悪化させる。なぜなら、それは任意のオリジンからさらにスクリプトを注入するためにこそ存在しているからだ。

現在主流のベストプラクティスは、レスポンスごとの nonce と strict-dynamic を組み合わせて構築する厳格な CSP だ。

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

strict-dynamic を使うと、ブラウザはすでに信頼されたスクリプトによって読み込まれたスクリプトを信頼する。これはまさにタグマネージャーが依存する挙動だ。ベンダーにとっては便利だが、あなたにとっては危険だ。ひとたび侵害されたタグが、あらゆるものを読み込む暗黙の許可を手にしてしまう。

SRI は、それを最も必要とするスクリプトには効かない

Subresource Integrity(サブリソース完全性)は、取得したファイルを実行する前に、暗号学的ハッシュと照合して検証させる仕組みだ。

<script
  src="https://cdn.example.com/tracker.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>

SRI は sha256sha384sha512 をサポートし、ハッシュが一致しない場合ブラウザはそのリソースを拒否する。これは侵害された CDN に対する明快な防御策だ。ただし、静的でバージョンが固定されたファイルに対してのみ有効だ。

アナリティクスではこれが崩れる。ベンダーのタグは意図的に可変だ。同じ URL のファイルはベンダーが機能を出荷するたびに変わるため、固定したハッシュは次のデプロイで収集を壊してしまう。実際のところ、チームはページに最も広く影響を及ぼすスクリプトに限って SRI を外すことになる。2025 年 10 月のサプライチェーンリスクの再評価が、安全でない SRI 実装を**重大度「高」**と格付けしたのは、まさにこの理由からだ。

Trusted Types が 2026 年 2 月に下限を引き上げる

問題の DOM-XSS の側面には、いまやブラウザレベルの答えがある。Baseline 2026 年 2 月時点で、Trusted Types は現行のブラウザ全体で利用可能になった。次のように指定して強制を有効にする。

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default;

これ以降、Element.innerHTML のような DOM シンクは生の文字列を拒否し、登録されたポリシーによって生成された値のみを受け付ける。

const policy = trustedTypes.createPolicy("default", {
  createHTML: (input) => DOMPurify.sanitize(input),
});
el.innerHTML = policy.createHTML(userInput); // ok
el.innerHTML = userInput;                     // throws TypeError

これは本当に強力だ。だが同時に、レガシーなアナリティクスやタグマネージャーのコードが日常的に違反するたぐいのルールでもある。なぜなら、それらのスクリプトは innerHTML に書き込み、<script> ノードを単なる文字列として注入するからだ。Trusted Types を有効にするということは、たいていまずアナリティクスベンダーを無効にすることを意味する。

ファーストパーティトラッカーは厳格なポリシーと戦わずに収まる

この衝突は、アナリティクススクリプトが小さく、自己完結し、自分自身のオリジンから配信されると消え去る。それ以上コードを読み込まないトラッカーは、strict-dynamic も、許可リストへのベンダーホストの追加も必要としない。

Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script';

これだけだ。サードパーティオリジンもなく、nonce のエスカレーションもなく、タグマネージャーのために切り出された例外もない。

Monoid のトラッカーはおよそ 2 KB で、依存関係はなく、DOM-XSS シンクを一度も呼び出さない。innerHTML に書き込まず、スクリプトを注入せず、Cookie やストレージを読み書きしない。ページビューごとに /collectkeepalive リクエストを 1 回送るだけで、それ以外はアイドル状態を保つ。

fetch('/collect', {
  method: 'POST',
  body: JSON.stringify({ site_id, path, referrer, screen_w }),
  keepalive: true,
});

ファイルが静的なので、SRI を適用しても決して壊れない。固定したハッシュは、あなたがスクリプトを更新すると決めるまで有効なままだ。そして識別子が一方向の日次ハッシュ SHA-256(IP | UA | SALT_SECRET | YYYY-MM-DD) であるため、緩い CSP が漏らしうるようなクロスサイトプロファイリングそのものが存在しない。

このパターンはアナリティクスの枠を超えて一般化できる。許可リストから取り除くすべてのスクリプトは、それとともに取り除かれる一群の XSS バイパスでもある。最も安全なサードパーティスクリプトは、読み込まないスクリプトだ。

Sources

Comments

Loading comments…