- はじめに
- 前提: Content ScriptとService Workerの違い
- 公式ドキュメントに基づくセキュリティ対策
- Content Scriptの信頼性が低い理由
- 対策
- チェックリスト
- まとめ
- 参考
はじめに
こんにちは!株式会社iimonでエンジニアをしている遠藤です。
最近、Chrome拡張機能のセキュリティについて調べる機会がありました。
そこで今回は、自分自身の整理を兼ねて、特にContent Scriptを扱う際のセキュリティ対策について、ベストプラクティスに沿ってまとめてみました!
本記事の内容はChrome拡張機能を実装する上での基本的な内容かもしれませんが、整理することでContent ScriptとService Workerの境界線をより意識できるようになりたいと思います!
※ 本記事はManifest V3を前提としています。
※ すべてのケースを網羅しているわけではないため、公式ドキュメントも併せてご確認ください。実際のプロジェクトでは、要件に応じた追加の検討が必要です。
前提: Content ScriptとService Workerの違い
Chrome拡張機能は基本的にContent ScriptとService Workerで構成されます。
| Content Script | Service Worker | |
|---|---|---|
| 役割 | DOM操作、ページ情報の取得 | バックグラウンド処理 |
| DOMへのアクセス | ◯ | × |
| Chrome API | 一部のみ | ◯ |
※ Content Scriptで利用可能なChrome APIについてはコンテンツ スクリプト | Chrome for Developersを参照
公式ドキュメントに基づくセキュリティ対策
参考までに、公式ドキュメントでは以下のようなセキュリティ対策が紹介されています。
- デベロッパーアカウントは2段階認証を有効にする
- HTTPSを使用する
- 拡張機能の権限を最小限に制限する
- マニフェストフィールドを制限する
- Content Scriptは慎重に利用する
- 入力されたデータを検証する
- manifestにCSPを明示的に登録する
- など
参考: Stay Secure
本記事では、『Content Scriptは慎重に利用する』に焦点を当て、なぜ慎重に利用すべきなのか、どのような対策が必要なのかを具体的なコード例を交えて解説していきます!
Content Scriptの信頼性が低い理由
Content Scriptは「Isolated World(ページや他の拡張機能からアクセスできないプライベート実行環境)」で実行されますが、以下の理由から信頼性が低いとされています。
1. Content ScriptがアクセスするDOMの信頼性が低い
Content ScriptはWebページのDOMを操作することができます。
しかし、悪意あるWebページがDOMの一部を操作したり、名前付きアイテムなどのウェブ標準の予期しない動作を悪用する可能性があります。
そのため、DOMから取得した値をそのまま信頼してはいけません。
また、「host_permissionsで信頼できるサイトのみに限定すれば安全では?」と思うかもしれませんが、信頼できるサイトでも以下の理由でDOMが改ざんされる可能性があります。
そのため、DOMの値自体を信頼しないという原則は変わりません。
2. Webページと同じレンダラプロセスで実行される
DOMを操作するために、Content ScriptはWebページと同じレンダラプロセス(JavaScriptの実行やレンダリングを行うプロセス)で実行されます。
Chrome
├── レンダラプロセス(タブA - Webページ用)
│ ├── Webページ ←── 攻撃者
│ └── Content Script ←── 同じプロセスなので影響を受ける
│
└── レンダラプロセス(拡張機能用 - 別プロセス)
└── Service Worker ←── 別プロセスなので影響を受けにくい
これにより、以下のリスクがあります:
サイドチャネル攻撃(Spectre等): 同じプロセス内のメモリを間接的に読み取られる可能性
https://spectreattack.comレンダラプロセスの侵害: 侵害されたレンダラはJavaScriptのルールや同一オリジンポリシーの制約を受けなくなり、Content Scriptが乗っ取られる可能性
Securing your extensions against compromised renderer processes
対して、Service Workerは信頼できないスクリプトの実行や外部画像の埋め込みを行わないことを前提としているため、レンダラプロセスの侵害はContent Scriptに比べて困難とされています。
対策
Content Scriptの信頼性が低いことを踏まえ、以下の対策が必要です。
1. Content Scriptからのメッセージは検証しサニタイズする
侵害されたContent Scriptからのリクエストでないことを確認するために、Service Worker側でアクション名やデータの検証を行います。
脆弱な実装例
// content.js const userData = document.getElementById("user").dataset.info; chrome.runtime.sendMessage({ action: "saveUser", data: userData });
// service-worker.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "saveUser") { // ❌ 危険:検証なしでストレージに保存 chrome.storage.local.set({ user: message.data }); } });
安全な実装例
// service-worker.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "saveUser") { // ✅ データ型の検証 if (typeof message.data !== "string") return; // ✅ JSONとしてパースして検証 try { const parsed = JSON.parse(message.data); // ✅ 必要なプロパティの検証 if (typeof parsed.name !== "string") return; if (typeof parsed.age !== "number") return; // ✅ 検証したプロパティだけを保存 chrome.storage.local.set({ user: { name: parsed.name, age: parsed.age } }); } catch (e) { // 不正なJSON return; } } });
ここでparsedオブジェクトをそのまま保存せず、必要なプロパティだけを取り出しているのは、parsedをそのまま保存すると攻撃者が追加した悪意のあるプロパティ(例: __proto__やその他の予期しないキー)も一緒に保存されてしまうためです。例では、必要なプロパティだけを明示的に取り出すことで、想定外のデータ混入を防ぐ実装にしています。
補足: 外部との通信(externally_connectable)
Chrome拡張機能は、外部のWebサイトや他の拡張機能からの通信も可能です。
この場合は、追加で送信元の検証を入れたり、manifest.jsonで受信できる送信元を限定しておくと効果的です。
// service-worker.js chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { // Webサイトからの通信:送信元オリジンの検証 if (sender.origin !== 'https://trusted-site.com') return; // 他の拡張機能からの通信:拡張機能IDの検証 if (sender.id !== 'abcdefghijklmnopqrstuvwxyz') return; });
// manifest.json { "externally_connectable": { "ids": ["abcdefghijklmnopqrstuvwxyz"], "matches": ["https://trusted-site.com/*"] } }
externally_connectableの設定について、詳細を知りたい方は下記のページを参照してください!
developer.chrome.com
externally_connectableキーが宣言されていない場合は、すべての拡張機能は接続できますが、Webページは接続できません。
2. Content Scriptから実行可能なアクションの範囲を制限する
Content Scriptから任意のURLや拡張機能APIに対する任意の値を受信しないように設計します。
脆弱な実装例
// content.js const productId = document.getElementById("product").dataset.id; // ❌ Content Script側でURLを構築してしまっている chrome.runtime.sendMessage({ action: "getProduct", url: `https://api.example.com/products/${productId}`});
// service-worker.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "getProduct") { // ❌ 危険:Content Scriptから受け取ったURLをそのまま使用 fetch(message.url); } });
Content ScriptからURLを直接受け取ると、レンダラプロセスが侵害されている場合に、攻撃者が任意のURLを指定できてしまいます。例えば、内部APIや認証エンドポイントへの不正なリクエストに悪用される可能性があります。
安全な実装例
// content.js const productId = document.getElementById("product").dataset.id; // ✅ URLではなく、必要最小限のパラメータのみを送信 chrome.runtime.sendMessage({ action: "getProduct", productId: productId });
// service-worker.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "getProduct") { // ✅ データの検証 if (typeof message.productId !== "string") return; if (!/^[a-zA-Z0-9]+$/.test(message.productId)) return; if (message.productId.length > 20) return; // ✅ URLはService Worker側で構築 fetch(`https://api.example.com/products/${message.productId}`); } });
URLはService Worker側で構築し、Content Scriptからは必要最小限のパラメータ(検証済みのID等)のみを受け取るようにします。
また、この例では正規表現/^[a-zA-Z0-9]+$/で英数字のみを許可することで、パストラバーサル(../)やURLエンコードを利用した攻撃(%2Fなど、%を含む文字列)、特殊文字の混入を防いでいます。
3. 機密情報はContent Scriptで扱わない
前述のように、Content ScriptはWebページと同じレンダラプロセスで動作するため、サイドチャネル攻撃やレンダラプロセスの侵害により、Content Script内のデータが攻撃者に読み取られる可能性があります。
公式ドキュメントにも以下のような記述があります。
コンテンツ スクリプトに送信されたデータはウェブページに漏洩する可能性があると想定してください。
メッセージ受け渡し | Chrome for Developers
※何を機密情報とするかは状況によりますが、ここでは例としてAPIキーやユーザーのフルネーム、住所などを機密情報と仮定して解説します。
脆弱な実装例
// service-worker.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "getUserProfile") { chrome.storage.local.get("userProfile", (result) => { // ❌ 危険:機密データをContent Scriptに送信 sendResponse({ name: result.userProfile.name, email: result.userProfile.email, address: result.userProfile.address }); }); return true; } });
// content.js chrome.runtime.sendMessage({ action: "getUserProfile" }, (response) => { // 受け取った機密データがWebページに漏洩する可能性がある console.log(response.email); });
安全な実装例
// service-worker.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "checkPremiumStatus") { // ✅ 機密データはService Worker内で処理し、必要な結果のみを返す chrome.storage.local.get("userProfile", (result) => { const profile = result.userProfile; // Content Scriptには判定結果のみ返す(APIトークンや会員情報は送らない) sendResponse({ isPremium: profile.membershipLevel === "premium", canAccessFeature: true }); }); return true; } });
// content.js chrome.runtime.sendMessage({ action: "checkPremiumStatus" }, (response) => { // 機密データ(APIトークン、メールアドレス等)ではなく、判定結果のみ受け取る if (response.isPremium) { document.getElementById("premium-badge").style.display = "block"; } });
4. XSS対策
DOMに表示する際はinnerHTMLではなくtextContentを使用します。
innerHTMLは文字列をHTMLとしてパースするため、悪意のあるスクリプトが含まれていると実行されてしまいます。一方、textContentは文字列をそのままテキストとして扱うため、スクリプトが実行されることはありません。
脆弱な実装例
// popup.js chrome.storage.local.get("userData", (result) => { // ❌ 危険:innerHTMLを使用 document.getElementById("display").innerHTML = result.userData; });
例えば、result.userDataに<img src=x onerror="alert('XSS')">のような文字列が含まれていた場合、innerHTMLではこれがHTMLとしてパースされ、onerrorイベントによりスクリプトが実行されてしまいます。
安全な実装例
// popup.js chrome.storage.local.get("userData", (result) => { // ✅ 安全:textContentを使用 document.getElementById("display").textContent = result.userData; });
textContentを使用すると、<img src=x onerror="alert('XSS')">という文字列がそのままテキストとして表示され、HTMLとしてパースされません。
※ HTMLタグを含むコンテンツをそのままHTMLとして表示する必要がある場合は、DOMPurify等のサニタイズライブラリの使用を検討してください。
チェックリスト
| チェック項目 |
|---|
| Content Scriptからのデータを検証しているか |
| アクション名を検証しているか |
| URLをService Worker側で構築しているか |
| 機密データをContent Scriptに送っていないか |
| 検証前のデータをストレージに保存していないか |
| innerHTMLを使用していないか |
| 外部通信の送信元を検証しているか |
まとめ
今回は、Content Scriptを扱う際のセキュリティ対策についてまとめてみました。
セキュリティの観点では、基本的にContent ScriptはWebページのDOM操作やデータ取得に限定し、それ以外の処理はService Workerに寄せるのが良いのかなと思いました。
公式ドキュメントの内容が中心になりましたが、整理することで個人的には学びがあったので、今後の開発に活かしていきたいと思います!
最後まで読んでいただきありがとうございます!
本記事には個人的な解釈も含まれています。記事の内容に誤りがありましたら、ご指摘いただけると幸いです。
また、弊社ではエンジニアを募集しております。
ご興味がありましたらカジュアル面談も可能ですので、下記リンクより是非ご応募ください!
iimon採用サイト / Wantedly