JavaScript

3rd-party JavaScriptとCSPの安全な扱い方 – 外部スクリプトを許可する実装ポイント

広告タグ、アクセス解析、チャットウィジェット、決済フォーム。いまのWebサイトは自社で書いていない外部(3rd-party)のJavaScriptで成り立っています。その外部スクリプトが乗っ取られると、閲覧者のブラウザ上で個人情報やカード番号が盗まれます。これを防ぐ最有力の仕組みがContent Security Policy(CSP)です。ただしCSPを有効にすると、今度は正当な外部スクリプトまで止まって「動かない」状態になりがちです。この記事は、外部スクリプトを止めずに危険なコードだけをブロックするCSPの設計を、script-srcのallowlist・nonce・hash・strict-dynamic・SRIという具体的な手段に落として解説します。

まとめ

結論として、外部スクリプトを安全に扱うCSPは「ホスト名のallowlistだけに頼らず、nonceまたはhashを主軸にする」設計に集約されます。要点は次の4つです。

  • CSPは読み込み元を宣言的に制限し、許可していないスクリプトの実行を止める。XSS(クロスサイトスクリプティング)の被害を軽減する最有力の多層防御。
  • ホスト名のallowlist方式は運用が破綻しやすい。リクエストごとのランダムなnonceか、スクリプト内容のsha256ハッシュで許可するのが現在の推奨。
  • 外部スクリプトがさらに別のスクリプトを読む場合はstrict-dynamicで信頼を伝播させる。CDNの改ざんはSRI(Subresource Integrity)で検知する。
  • いきなり本番でブロックすると事故る。Content-Security-Policy-Report-Onlyで違反レポートを集めてから本番ヘッダへ切り替える。

以降で、外部スクリプトのリスクからCSPの仕組み、具体的な許可の設計、動かないときの確認手順までを順に見ていきます。

外部スクリプト(3rd-party JavaScript)とは何か

外部スクリプト(3rd-party JavaScript)とは、自社ドメイン以外の第三者が提供・配信するJavaScriptのことです。Googleタグマネージャー、Google Analytics、広告配信タグ、A/Bテストツール、SNSの埋め込み、チャットボット、決済SDKなどが該当します。自社サーバーに置いた自前のスクリプト(1st-party)と対比して「3rd-party」と呼びます。

読み込みの仕組みとファーストパーティとの違い

外部スクリプトは、多くの場合scriptタグのsrc属性に配信元のURLを書くだけで読み込まれます。ブラウザは指定されたドメインからJavaScriptを取得し、自社サイトと同じ権限(同一オリジン)でそのコードを実行します。ここが要点です。外部のコードであっても、いったん読み込まれれば自社ページのDOM、Cookie、フォーム入力に自由にアクセスできます。iframeで隔離される埋め込みと違い、直接script読み込みは権限の壁がありません。

外部スクリプトが持ち込むセキュリティリスク

リスクは「自分たちのコードは安全でも、配信元が汚染されると被害が出る」点にあります。攻撃者が配信元CDNやツール提供元に侵入してスクリプトを差し替えると、その1行の変更が全訪問者のブラウザで実行されます。これはサプライチェーン攻撃と呼ばれ、決済ページに仕込まれてカード情報を外部サーバーへ送信するMagecart型の被害が代表例です。加えて、自社ページにXSSの穴があれば、攻撃者が注入したインラインスクリプトも同じ経路で動きます。つまり守るべき対象は「外部から読むスクリプト」と「ページに注入されるスクリプト」の両方で、CSPはこの両方に効きます。

Content Security Policy(CSP)の仕組みとXSS対策

Content Security Policy(CSP)とは、ブラウザに対して「このページで読み込みと実行を許可するリソースの出所」を宣言するセキュリティ標準です。サーバーがレスポンスヘッダで許可リストを渡し、ブラウザがそれ以外のスクリプト・スタイル・画像・通信をブロックします。許可していないインラインスクリプトが実行されないため、XSS攻撃で注入されたコードが動かず、被害を軽減できます。

ヘッダとmetaタグ、2つの配信方法

CSPの適用方法は2つあります。1つはHTTPレスポンスヘッダのContent-Security-Policy、もう1つはHTML内のmetaタグです。本番運用ではヘッダ方式を選びます。metaタグ方式はframe-ancestorsreport-urireport-tosandboxといったディレクティブが使えず、違反レポートの収集もできないためです。ヘッダはWebサーバー(Nginx/Apache)、リバースプロキシ、CDN、アプリケーションフレームワークのいずれの層でも付与できます。

主要ディレクティブの役割

CSPは「ディレクティブ名 許可ソース」の組を;で並べて書きます。外部スクリプトの制御で押さえるべきディレクティブは次のとおりです。

ディレクティブ 制御対象 未指定時のフォールバック
default-src 全リソースの既定値
script-src JavaScriptの読み込み・実行元 default-src
style-src スタイルシートの適用元 default-src
connect-src fetch・XHR・WebSocketの接続先 default-src
img-src 画像の読み込み元 default-src
frame-ancestors 自ページを埋め込める親 フォールバックなし
base-uri / object-src base要素 / plugin base-uriは無制限

script-srcを指定しない場合はdefault-srcの値が使われるため、まずdefault-src 'self'で全体を自社オリジンに絞り、外部スクリプトの分だけscript-srcで個別に開けるのが基本形です。

CSPが外部スクリプトをブロックする理由と許可の設計

CSPを有効にすると、既定では外部ドメインのスクリプトもページ内のインラインスクリプトも実行されなくなります。これはXSSで注入されたコードを止めるための正しい挙動です。問題は、正当な外部スクリプトも同じ理由で止まること。ここを「どう安全に開けるか」がCSP設計の中心です。

ホスト許可リスト(allowlist)方式とその限界

最も素朴な許可方法は、script-src 'self' https://cdn.example.comのように信頼するドメインを列挙するallowlist方式です。設定は簡単ですが限界があります。許可したドメインが*.googleapis.comのように広いと、その配下にある攻撃に悪用可能なJSONPエンドポイントまで許可され、CSPをすり抜けられることが知られています。ツールが増えるたびに許可ドメインが膨れ、実質「何でも許可」に近づくのも運用上の弱点です。allowlistだけに頼る設計は避けるべきで、これがGoogle等がnonce/hash方式を推奨する理由です。

nonce・hashによるスクリプト許可

allowlistの弱点を補うのがnonceとhashです。nonceは、リクエストごとにサーバーが生成する暗号論的にランダムな使い捨ての値です。CSPヘッダに'nonce-値'を書き、同じ値をscriptタグのnonce属性に付けると、その値を持つスクリプトだけが実行を許可されます。攻撃者は次のリクエストのnonceを推測できないため、注入したスクリプトは動きません。hashは、インラインスクリプトの中身をSHA-256などでハッシュ化した値(例'sha256-...')を許可リストに書く方式で、内容が1文字でも変われば不一致で弾かれます。ビルド時に確定する固定のインラインコードにはhash、動的に変わるページにはnonceが向きます。重要な補足として、nonceかhashをscript-srcに書くと、CSP Level 2以降のブラウザは'unsafe-inline'を無視します。古いブラウザ向けに'unsafe-inline'を併記しても、対応ブラウザではnonce/hashが優先され安全側に倒れます。

strict-dynamicで外部スクリプトの動的読み込みを許可

外部スクリプト(タグマネージャー等)は、自身がさらに別のスクリプトを動的に読み込むことがあります。nonceは親スクリプトにしか付いていないため、そのままでは子スクリプトが弾かれます。これを解決するのがCSP Level 3の'strict-dynamic'です。nonceまたはhashで信頼したスクリプトが読み込む子スクリプトへ、信頼を自動で伝播させます。'strict-dynamic'を書くと、ホスト名ベースのallowlistと'unsafe-inline'は無視され、信頼の起点はnonce/hashだけになります。対応はChrome 52以降、Edge 79以降、Firefox 52以降、Safari 15.4以降です。未対応の古いブラウザ向けにはallowlistを併記しておくと、対応ブラウザはstrict-dynamic、非対応はallowlistへ自動でフォールバックします。

外部スクリプトの改ざん検知と違反監視

CSPは「どこから読むか」を制御しますが、許可した配信元から届いたスクリプトが途中で改ざんされていないかまでは保証しません。ここを埋めるのがSRIによる改ざん検知と、Report-Onlyによる段階導入です。

SRI(Subresource Integrity)でCDN改ざんを検知

SRIは、外部から読むスクリプトの内容が期待どおりかをブラウザが検証する仕組みです。scriptタグにintegrity属性でハッシュ値を、crossorigin="anonymous"を付けると、ブラウザは取得したファイルのハッシュを計算し、一致しなければ実行を拒否します。これによりCDNが侵害されてファイルが差し替えられても被害を防げます。ハッシュはSHA-256・SHA-384・SHA-512に対応し、セキュリティと処理負荷のバランスからSHA-384が推奨されます。CSPのnonce/hashが「読み込みを許可するか」を決めるのに対し、SRIは「届いた中身が正しいか」を検証する補完関係にあります。バージョンが頻繁に上がるスクリプトはハッシュ更新の運用コストが増えるため、更新頻度で採否を判断します。

Report-Onlyとレポート収集で段階導入

いきなりContent-Security-Policyを本番に出すと、想定漏れで正当なスクリプトまで止まり障害になります。そこでContent-Security-Policy-Report-Onlyヘッダを使います。これは違反を検出してレポートを送るだけで、実際のブロックはしません。数日〜数週間レポートを集めて許可漏れを洗い出し、問題がなくなってから本番ヘッダへ切り替える、という段階導入が定石です。レポートの送信先はreport-toディレクティブとReporting-Endpointsヘッダで指定します。report-toが推奨で、旧来のreport-uriは非推奨ですが、ブラウザ対応の都合から当面は両方を併記しておくのが無難です。なおReport-Onlyはmetaタグでは使えないため、必ずヘッダで指定します。

CSPヘッダの設定例と外部スクリプト対応

ここまでの手段を1つのヘッダにまとめると、外部スクリプトを許可しつつ安全側に倒したCSPは次のような形になります。実際のヘッダ値は1行で出力します(下記は可読性のため改行しています)。nonceの値はリクエストごとにサーバーで生成した実際のランダム値に置き換えて出力します。なおreport-toを使う場合は、別途Reporting-Endpointsヘッダで送信先URLを定義する必要があります。

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0mBase64' 'strict-dynamic' https://cdn.example.com;
  style-src 'self';
  connect-src 'self' https://api.example.com;
  img-src 'self' data:;
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none';
  report-to csp-endpoint;
  report-uri /csp-report

スクリプト側は、サーバーが生成した同じnonceをタグに付けて出力します。外部スクリプトの読み込みも自前のインラインも、このnonceを持つものだけが実行されます。

<script nonce="r4nd0mBase64" src="https://cdn.example.com/widget.js"></script>

WordPressやReact、Djangoといったフレームワークでは、テーマ・ミドルウェア・サーバー設定のいずれの層でこのヘッダとnonce注入を行うかが実装の分かれ目になります。共通するのは、nonceをリクエストごとに生成してヘッダとタグの両方へ差し込む処理が必要という点です。静的なヘッダ文字列をコピーするだけでは、nonceが固定値になり安全性が失われるため注意します。

CSPで外部JavaScriptが動かないときの確認手順

CSP導入で最も多いトラブルが「外部スクリプトが動かない」「ページの一部が真っ白」という症状です。原因の切り分けは、ブラウザのデベロッパーツールのConsoleに出る違反メッセージから始めます。競合記事は設定方法までで終わりがちですが、実務ではこの復旧手順が要点です。

  • Consoleの違反メッセージを読む:「Refused to load the script … because it violates the following Content Security Policy directive: script-src …」の形で、どのURLがどのディレクティブで拒否されたかが出ます。まずどのソースが足りないかを特定します。
  • 不足ソースを許可に追加:外部ドメインが拒否されていれば、そのホストを許可するか、strict-dynamicを使っているなら親スクリプトにnonceが付いているかを確認します。
  • インラインスクリプトの拒否:「refuse to execute inline script」はインラインコードがnonce/hashなしで書かれているサインです。nonceを付けるか、外部ファイルに切り出します。
  • eval由来の拒否:一部ライブラリは内部でevalnew Functionを使います。これは'unsafe-eval'が必要ですが、安易な許可はXSS耐性を下げるため、evalを使わない代替ライブラリを優先します。

切り分けの近道は、本番へ出す前にContent-Security-Policy-Report-Onlyで同じポリシーを流し、レポートに出る違反を潰しておくことです。ブロックせずに漏れだけ見えるため、動かない事故を本番前に防げます。

よくある質問(FAQ)

CSPとは何ですか。わかりやすく教えてください

CSPは、Webページが読み込んでよいスクリプトや画像などの「出所」をサーバーがブラウザに宣言する仕組みです。宣言していない出所のスクリプトはブラウザが実行を拒否します。これにより、攻撃者がページに勝手なJavaScriptを注入するXSS攻撃が成立しにくくなります。玄関に「登録済みの業者以外は入れない」という名簿を貼るイメージです。

CSPで外部スクリプトを許可(ホワイトリスト)するには

script-srcディレクティブに許可したいドメインを列挙します。ただしドメイン列挙(allowlist)方式は許可範囲が広がりやすく、抜け道も知られています。より安全なのは、リクエストごとのnonceかスクリプト内容のhashで許可する方式です。外部スクリプトがさらに子スクリプトを読む場合は'strict-dynamic'を併用します。

CSPを設定したらインラインスクリプトが動かなくなりました

CSPは既定でインラインスクリプトをブロックします。復旧には、そのスクリプトにサーバー生成のnonceを付けるか、内容のSHA-256ハッシュをscript-srcに登録します。安易に'unsafe-inline'を付けるとXSS防御が実質無効になるため避けます。nonceかhashを併記すれば、対応ブラウザは'unsafe-inline'を無視して安全側に動きます。

ReactやWordPressでCSPを入れるときの注意点は

フレームワークを問わず、nonceはリクエストごとに生成し、CSPヘッダとscriptタグの両方に同じ値を注入する必要があります。静的な文字列としてヘッダに固定nonceを書くと、値が推測可能になり意味を失います。ビルド時に確定するバンドルにはhash、サーバーレンダリングで毎回変わるページにはnonceが適します。

SRIとCSPはどちらを使えばよいですか

役割が違うため併用します。CSPは「どの出所から読み込むか」を制御し、SRIは「読み込んだファイルの中身が改ざんされていないか」を検証します。CDNが侵害されてファイルが差し替えられた場合、CSPだけでは防げずSRIが必要です。更新頻度の高いスクリプトはSRIのハッシュ更新コストが上がるため、更新頻度を見て採否を決めます。

関連記事

資料請求

RELATED POSTS 関連記事