🐕

CSPでサードパーティースクリプトを律する

2024/02/18に公開

はじめに

Legalscapeの顧客の中には、情報セキュリティー等の理由から社内ネットワークからの通信の宛先を制限している組織もたくさんいます。

そのためLegalscapeでは、プロダクトの動作に必要な第三者リソースの一覧を管理し、Legalscapeの導入時にはそれらのドメイン名への接続を許可するようにお願いしてきました。

しかし、現代のWeb開発は、第三者リソースが利用可能であることを暗に期待しがちです。開発者がLegalscapeの顧客背景をよく知らずに新しい依存を導入してしまうことも考えられます。またさらに厄介なのが間接依存の増加です。実際に、firebase packageの更新によって内部で呼び出しているAPIのエンドポイントが変化し、開発者が知らないうちに接続先が変わっていたということが判明しています。[1]

そこで私は、CSPを使うことでサードパーティースクリプトやAPIを含む第三者リソースをより厳格な制御下に置くことを目指しました。

CSPとは

CSP (Content-Security-Policy) はセキュリティー関連のHTTPヘッダーのひとつで、ページ内のコンテンツが行える操作 (リソースの読み込み等) の許可リストを宣言することで、ページが利用可能な機能をあえて制限することができます。

CSPの重要な目的のひとつはXSSのリスクを軽減することにあります。他のセキュリティー対策にCSPを併用すると、XSSにつながるような問題のある実装があった場合でも、その影響をなるべく小さくすることができます。また、クリックジャッキングを防ぐための基本的な設定もCSPに含まれています。

本稿で取り扱う「第三者リソースの制御」はCSPの主要な目的ではないものの、CSPの機能の恩恵を受けることができます。もちろん、プロダクト内で利用する第三者リソースをなるべく少なくし制御化に置くことは、セキュリティーリスクの軽減にもある程度は効果があるでしょう。

CSPの2つの運用

CSPのポリシー違反に対応する方法は2つあります。

  • ひとつは、ポリシー違反となる動作を実際に拒否することです。
  • もうひとつは、ポリシー違反を記録することです。

この2つの方法は排他的ではありません。いずれか1つだけを運用することも可能ですし、両方を同時に行うことも可能です。

  • ポリシー違反となる動作を拒否するには Content-Security-Policy ヘッダーを利用し、拒否しない場合は Content-Security-Policy-Report-Only ヘッダーを利用します。ヘッダーが分割されているため、強制ポリシーと寛容なポリシーを併存させて段階的な運用をすることができるようにもなっています。
  • ポリシー違反を記録するには、各ヘッダーに report-to または report-uri 指令を指定します。

default-src 指令とその運用

CSPの指令の大部分は「リソースの読み込み先を制限する」という形態を取っています。特に、frame-ancestorsについで最もよく使われる指令は以下のようなものでしょう。

Content-Security-Policy: script-src https://cdn.example.com/

これはJavaScriptコードの読み込み先を https://cdn.example.com/ に制限するもので、それ以外の方法で指定されたJavaScriptコードの実行を制限します。

このように、セキュリティー目的でCSPを運用する場合はリソースの種別ごとに制限を書くのが一般的だと考えられます。しかし、今回はこれらのリソース種別には関係なく、とにかく通信が行われることが問題になります。そこで登場するのがdefault-srcです。

default-srcは名前の通り、 *-src 系の指令のデフォルト値を指定します。この指令だけを指定しておけば、全てのリソース種別において一様に制限が適用されます。もちろん、あとからセキュリティー目的でCSPを強化する必要が出てきても、この指定をリソース種別ごとにオーバーライドして対応できます。

report-to指令とreport-uri指令

ポリシー違反の記録をとるにはreport-to指令またはreport-uri指令を使います。

Content-Security-PolicyまたはContent-Security-Policy-Report-Onlyヘッダにこの指令が含まれていると、Webブラウザはポリシー違反情報を指定されたエンドポイントに所定の形式でPOSTします。

report-toとreport-uriの目的は同じであり、report-toは単にreport-uriの新しいバージョンです。そのため、本来であれば非推奨化されたreport-uriではなくreport-toを使うのが望ましいです。しかし、これには以下の問題があります。

  • Firefoxはreport-toを実装していない。
  • Chromeはreport-toを実装しており、Report-To: ヘッダーと Reporting-Endpoints: ヘッダーの両方に対応しているはずだが、手元での実験ではいずれも期待したように動作しなかった。うまくいかなかった理由は現在のところわかっていない。

Firefoxのようにreport-toを実装していないブラウザを考慮する場合、report-uriとreport-toの両方を指定することも可能で、この場合report-toを解釈可能なブラウザではreport-toが優先されます。本来は両方を指定するのがよいはずですが、上記のようにChromeで正しく動作しないreport-toを指定するとreport-uriも無視されてしまうため、今回はreport-uriのみを指定するようにしました。

Reporting API

report-to/report-uriのほかに、Reporting APIを使ってポリシー違反を取得することもできます。これはCSPのポリシー違反のようにブラウザ上で発生したイベントに関する情報をJavaScriptから購読できるAPIです。

今回はより確実に違反情報を取得するために、report-to/report-uri指令による方法を利用しました。

metaタグとの関係

CSPはmetaタグでも指定できますが、利用できる機能が制限されています。report-uriが使えないのももちろんですが、Content-Security-Policy-Report-Onlyが使えないのが致命的であるため、今回はmetaタグではなく正規のHTTPヘッダーを利用することにしました。

違反を収集する

違反を収集するエンドポイントは自分で作ってもいいですが、今回はSentryのCSP違反収集機能を利用することにしました。Sentryが提供するエラーの分類・検索・統計・アラートなどの機能をそのまま使えることが大きな利点です。

CSPの運用

前述のように、CSPは状況にあわせて異なる厳格度で運用できます。基本となるのが以下の2段階の導入手順です。

  1. ポリシー違反の記録のみを行う。
  2. 記録をもとに安全と判断したら、ポリシーの強制を行う。

今回のプロジェクトの目的である「第三者リソースの制御」にも、これに対応して異なる段階のゴールが想定されていました。

  1. ポリシー違反の記録を監視することで、第三者リソースへの依存が増えたときにリアクティブに必要な対処を行う。
  2. 開発環境でポリシーを強制することで、開発者が意図せず第三者リソースへの依存を増やさないようなプロアクティブな防御を行う。
  3. 本番環境でポリシーを強制することで、意図しない第三者リソースを利用していないことを完全に保証する。

レベル1のゴールだけでも有益であることから、とりあえずはポリシー違反の記録をする形での導入を行いました。その結果わかったのは、顧客側の組織や個人の設定に由来する第三者リソース依存があり、それらもCSPのポリシー違反になることです。たとえばSaaSツールが提供するブラウザ拡張がこの例にあたります。

結論として私たちは、こういったリソースの違反を全て拒否することでユーザーから見たプロダクトの可用性にどれくらいの影響があるのか読めず、そこまでしてレベル3のゴールを達成する必要は現段階ではないと判断しました。そこで、主要サービスでレベル2の運用状況を達成することを合意し、そこまで進めたことで本プロジェクトは完了しました。

運用でわかったこと

その他にも、実際にCSPを運用してわかったことがいくつかあるのでここに記載しておきます。

iframeの間接依存

サードパーティースクリプトの中には、文書中にiframeを挿入するものがあります。親フレームのCSPはiframe内の文書には影響を与えないため、iframeを経由した間接依存へのリクエストを拒否したり記録したりすることはできないようです。

実験的なAPIとして、iframe要素のcsp属性により親フレームからiframe内の文書にCSPを適用することができるようなので、サードパーティースクリプトのiframe作成処理に割り込んでcsp属性を設定することができれば、この問題を解決する余地もあるかもしれません。

特別なソース

httpsドメイン名や 'self' 指定のほかに、以下のような許可を出す必要がないか検討するのがいいでしょう。

  • 'unsafe-eval', 'wasm-unsafe-eval', 'unsafe-inline' ... これらを禁止することはXSSのリスク軽減には非常に有用ですが、副作用も大きいです。もし現時点でこれらのソースを禁止する意図がなければ、許可リストに明示しておくといいでしょう。
  • 'data:', 'blob:' ... ローカルで画像データを作って表示する場合など、dataやblobは意外と広範囲で使われています。これもscript-srcで制限するのは場合によって有意義ですが、default-srcでは許可しておいたほうが無難かもしれません。

偽陽性

CSPの報告はWebブラウザが自己判断で送信するものであり、必ずしも信頼できる情報だけが送信されるとは限りません。

特に不思議なのが、CSPで明示的に許可しているはずのドメインへのアクセスがブロックされた記録が多数送信されている点です (報告データ内にCSPの記述が含まれており、その記述と自己矛盾しています)。もしかすると、ページ側ポリシー以外の理由でブロックされたものをCSPの報告エンドポイントに送信するような実装があるのかもしれません。

Sentryはデフォルトで有益でない報告をフィルタする機能がついているはずですが、このような自己矛盾する報告も除外してよいのではないかという気がします。

トラッカー

広告やアクセス解析など、マーケティングに使われる第三者ツール群の中には、非常に多様なドメイン名からリソースを取得する行儀の悪いものがいくつかありました。こういったスクリプトはGTMなどのタグマネージャを介して呼び出されることが多く、一般的に開発チームの制御下にないので、そのことも問題を困難にしています。

Googleが提供するServer containerはこの問題への対応として使えそうだと見込んでいます。

フレームワークへの適用

具体的にCSPヘッダを適用する方法はフレームワークや実行環境により異なります。私たちのプロジェクトでは以下の2つのパターンがありました。

  • SPA構成のNuxt.jsプロジェクト。このプロジェクトでは、ローカル開発時はNuxtの開発用サーバーを利用しますが、デプロイ時は静的アセットをFirebase Hostingにアップロードしています。
    • 同じCSP設定を両方の形態で適用するために、CSP設定を算出して返す関数をTypeScriptで書き、これを2つのモジュールから利用するようにしました。
      • ひとつはNuxt.jsのサーバーミドルウェアです。
      • もうひとつはFirebase Hostingの設定を行うCLIツールです。Firebase Hostingでは、アップロード時に firebase.json に記載されたヘッダーが適用されます。そこで、 firebase.jsonfirebase.json.template にリネームし、そこに算出したCSP設定を注入して動的に firebase.json を生成するようにしました。
  • SSR構成のNuxt.jsプロジェクト。このプロジェクトでは常にNuxtのサーバーを利用します。
    • この場合はNuxt.jsのサーバーミドルウェアさえあればOKです。Sentryの設定は、ローカル向けのdotenvのほかに、デプロイ先のサーバー (App Engine) の環境変数に記載する必要があります。

まとめ

  • CSP (Content-Security-Policy) は主にXSSのリスク軽減を目的として、ページの権限を制限するヘッダーである。
  • Legalscapeでは制限の強いネットワークでもプロダクトを利用しやすいように、第三者リソースへの依存をなるべく少なくしたい。従来これは完全にマニュアル管理されていたが、CSPを流用することである程度管理を自動化して、より早期に問題に対応できるようになった。
  • CSPを運用してみると、サービス利用環境の多様性や報告の偽陽性など、いくつかのマイナーな困難はあったが、当初の目的はおおむね達成することができた。

最後に

本記事では私がLegalscapeで業務委託として関わっていたプロジェクトの取り組みを説明しました。

Legalscapeは国内リーガルテック企業としては特異的な立場にあり、なかなか面白いプロダクトを作っています。メンバーも強者揃いですので、ぜひ興味があれば以下のページを覗いてみてください。

https://legalscape.notion.site/09aeb478072946c18249495b8fb63fcd

脚注
  1. さいわい、これは同じ *.googleapis.com 内での移動であったため、おそらく実際の影響はなかったのではないかと思います。 ↩︎

Legalscape(リーガルスケープ)

Discussion