CSP(Content Security Policy)を強化した環境で Google Analytics(GA4)やGoogle Tag Manager(GTM)を安全に動かすのは、実務でも難易度が高いテーマです。
やりたいことは単純で、Express フレームワーク上に構築した Web アプリケーションに Google Analytics のような解析ツールを導入することです。
一見すると簡単に見えますが、実際に導入すると Tag Assistant が Not Connected になる問題がよく起こります。
スクリプトを貼り付けるだけなのにどうして・・・
結論から述べると CSP と Google ツールは正しく設定しないと設計通りに動作しません。
Google Analytics とそれに付随する Tag Assistant / GTM は iframe や postMessage などの動的スクリプト挿入を多用します。
一方 CSP は許可されたスクリプト以外は全てブロックする仕組みです。
なんか最初から仕様が衝突しているような印象を受けますが、何も考えずに導入すると Google ツールが CSP に妨害されて動作しないことがよく起こります。
これに対する対処として
‘unsafe-inline’
‘unsafe-eval’
を追加して解決する方法が提案されているのを目にしますが、これはセキュリティ的には後退です。
より安全で実務的な構成としては nonce + strict-dynamic のほうが適しているのではないかと思うわけです。
そもそもの原因として GA4 / GTM は inline script を生成し、外部ドメインから追加ロードを行い、動的に <script> を挿入します。
この動的実行を CSP の静的ホワイトリストが妨害するわけです。
そのため、インラインスクリプトの実行許可が解決案にあげられることがあります。
script-src ‘self’ ‘unsafe-inline’
確かに無条件の実行許可を与えることで、すべてのインラインスクリプトの実行が可能となります。同時にユーザー入力経由の XSS もそのまま実行を許可されてしまいます。
CSP の存在理由は、どのスクリプトを信頼するかを明示的に指定することなので、構成の意義も薄れてしまいます。
XSS 耐性を維持しつつ、GA4 / GTM のインラインスクリプトの実行を許可する設定が strict-dynamic です。
script-src 'nonce-xxxx' 'strict-dynamic'
この設定は nonce 付きスクリプトが読み込んだスクリプトは信頼するという「信頼の連鎖」を活用します。
具体的の実装としては HTML に埋め込むスニペットスクリプト(GA4 や GTM)に nonce を付与します。
<script async src="https://www.googletagmanager.com/gtag/js?id=*-**********" nonce="<%= nonce %>"></script>
Google ツールはそこから動的に他のスクリプト(広告タグなど)をロードする仕組みになっているので、strict-dynamic が指定されていれば、GA4 や GTM が呼び出す後続のスクリプトも自動的に許可されます。
strict-dynamic の仕様として最初のスクリプトが信用されていれば、後続は自動的に許可されるため、GTM によって動的に生成されるタグを一つずつホワイトリストに登録しなくても正常に動作するようになります。
逆に言えば、上記の設定では nonce なしでは inline script は実行できません。
GTM / GA4 / Tag Assistant は nonce を付けずに inline script を挿入しようとすることもありますが、strict-dynamic 下では「信頼された親」から生まれた子スクリプトであれば、nonce なしでも実行が許可されるようになります。
Node.js (Express) での実装例
以下は、helmet ミドルウェアを使用して nonce を生成し、CSP を適用する実装例です。
実装例
const crypto = require('crypto');
const helmet = require('helmet');
// Generate nonce for each request
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
// Configure Helmet with CSP
app.use(
helmet({
contentSecurityPolicy: {
directives: {
"script-src": [
"'self'",
(req, res) => `'nonce-${res.locals.nonce}'`, // <- the generated nonce
"'strict-dynamic'",
"https:", // fallback for old browsers
"http:" // fallback for old browsers
],
"object-src": ["'none'"],
"base-uri": ["'none'"]
}
}
})
);
ブラウザの対応状況と注意点(2026年)
- Chrome 52+, Edge 79+, Firefox 52+ → strict-dynamic に対応しており、上記設定で意図通り動きます。
- 一部古いブラウザ → strict-dynamic 非対応。その場合、https: や http: の記述がフォールバックとして機能し、ホワイトリスト方式として動作します。
- Safari → strict-dynamic は対応していますが、一部のバージョンで挙動が不安定になるケースが報告されています。ほんと、林檎はさあ・・・
まとめ
- unsafe-inline で無条件にスクリプトの実行を許可するのを止める
- GA4 / GTM のエンドポイントに nonce を設定にする
- strict-dynamic によってインラインスクリプトを連鎖的に許可する
これでセキュアな構造を維持しつつ、便利な Google ツールを使用できるようになるはず。
参照:
Use Tag Manager with a Content Security Policy | Google for Developers
“strict-dynamic” | Can I use… Support tables for HTML5, CSS3, etc
