🙆

Chrome拡張機能のセキュリティリスクについて調べた備忘録

に公開

TL;DR

  • Chrome 拡張機能を調べていて、Manifest V3 のセキュリティリスクについて調べた備忘録。
  • 今から新しく拡張機能を作るなら Manifest V3 で作る。
  • 調べながら書いているので、間違いがあれば指摘してもらえると助かります。

Manifest とは何か

Chrome 拡張機能の“権限・メタデータ”を宣言する JSON ファイル。
このファイルがないとストアにアップロードできません。

v3 の manifest は例えばこんなやつ

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "description": "A sample Chrome extension",
  "permissions": ["storage", "tabs"],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html"
  }
  "content_scripts": [
    {
      "matches": ["https://example.com/*"],
      "js": ["content.js"]
    }
  ],
}

v2 はこんなやつ

{
  "manifest_version": 2,
  "name": "My Extension",
  "version": "1.0",
  "description": "A sample Chrome extension",
  "permissions": ["storage", "tabs"],
  "background": {
    "scripts": ["background.js"],
    "persistent": true
  },
  "browser_action": {
    "default_popup": "popup.html"
  }
}

永続バックグラウンドページ

v2 では 仕様として background":{"persistent":true} が許可されており、拡張機能が常にバックグラウンドで実行されることが可能でした。これにより、リソースの無駄遣いやパフォーマンスの低下が発生する可能性がありました。悪用されたのは仮想通貨のマイニングや、ユーザーのブラウジングデータを収集するためのバックグラウンドスクリプトでした。ユーザーはタブやタスクマネージャーからは気づきにくいので、拡張機能がバックグラウンドで動作していることに気づかないことが多かったらしいですね。
しかし v3 では、background":{"persistent":true} が非推奨になり、代わりに Service Worker ベースのバックグラウンドスクリプトが導入されました。これにより、拡張機能は必要なときだけ実行されるようになり、リソースの無駄遣いが減ります。

eval / インライン JS` の許可

v2 では文字列をそのまま実行 (eval、new Function) したり、CDN から取得したスクリプトを <script> 挿入で動的ロードすることが可能でした。
eval の危険性についてはこちら
v3 では、eval や new Function などの動的コード実行が禁止されるのと、今まで CDN から JS を import 可能でしたが、それも不可能になり、すべてパッケージ内に同梱することが必要になりました。

Manifest V3 でもまだセキュリティリスクはある

Manifest v3 である程度改善したものの、まだセキュリティリスクは残っています。

権限まわり

v2 ではインストール時の権限同意を求める際に、必要以上の権限を要求することが可能でした。これにより、ユーザーが拡張機能の実際の機能を理解せずにインストールしてしまうリスクがありました。例えば cookies + <all_urls> でセッションハイジャックが可能になるなど。そのため、「乗っ取られたら即アウト」という拡張が大量に生まれ、サプライチェーン攻撃(開発者アカウント乗っ取り → 不正アップデート)がよくあったようです。
v3 では host_permissions と Chrome UI の「サイトごとの権限 ON/OFF」機能でユーザ側が制限しやすくなりましたが、依然として開発者が広い権限を宣言すことはできます。
つまり、V3 は ユーザーが“最小権限” を設定する UI を提供しただけで、開発者が過剰権限を宣言する自由は残ります。

Message Passing Injection

拡張機能内のさまざまなコンポーネント間でメッセージを送信するだけでなく、Messaging API を使用して他の拡張機能と通信することもできます。これにより、他の拡張機能が使用できる公開 API を公開できます。

対策としては以下が考えられます。

対策 ①: sender.id/sender.url をチェックして、許可された拡張機能からのリクエストのみを受け入れるようにする。

// example
chrome.runtime.onMessageExternal.addListener((req, sender, sendResponse) => {
  if (sender.id !== '許可した拡張ID') return;
  if (req.command === 'getSecrets') sendResponse({ secret: fetchSecret() });
});

対策 ②:externally_connectable で許可 ID/Origin をホワイトリスト化する。

// manifest.json
{
  "externally_connectable": {
    "matches": ["https://example.com/*"],
    "ids": ["許可した拡張ID"]
  }
}

externally_connectable を設定すれば送信元 id を検証しなくてもいいのでは?と思うかもしれませんが、これはあくまで「許可した拡張機能からのリクエストのみを受け入れる」だけで、悪意のある拡張機能が許可されたドメインからリクエストを送信することは可能です。
例えば matches で許可した正規ドメインでも、サブドメイン乗っ取りなどが起きると、悪意のある拡張機能がそのドメインからリクエストを送信することが可能です。

対策 ③:web_accessible_resources を最小限にする。

web_accessible_resources で列挙したファイルだけは chrome-extension://<id>/path で読めるようになります。つまり、悪意のある拡張機能や攻撃者がそのファイルにアクセスできるようになります。
web_accessible_resources に入れたページ/スクリプトは、どんな Web サイトからでも読み込めます。
そして、CORS(オリジン制限)や CSP(外部スクリプト禁止ポリシー)を素通り してしまうため、通常のサイトより攻撃者にとって扱いやすい格好の的になり得ます。

// manifest.json
{
  "web_accessible_resources": [
    {
      "resources": ["allowed-script.js"],
      "matches": ["https://example.com/*"]
      // matches:["<all_urls>"] は機能上は許可されているが原則禁止にする
      // use_dynamic_url: true`
    }
  ]
}

use_dynamic_url: trueも 1 つの手段なり得ます。true の場合、 ID が 起動ごとにランダム ID になるため、攻撃者が特定の拡張機能のリソースにアクセスすることが難しくなります。

Content Script XSS

以下サンプルの概念(例としては単純なものです)

アプリ側(本来はページにコメントを表示するだけの安全なロジック)

// 悪例
/// content.js
const comment = document.querySelector('#comment').textContent;
document.body.insertAdjacentHTML('beforeend', comment); // またはinnerHTML = comment

// postMessage でバックグラウンドへ橋渡し
window.addEventListener('message', (e) => {
  chrome.runtime.sendMessage({ cmd: e.data.cmd });
});

// -----------------------------------------------

//// background.js
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
  if (req.cmd === 'dumpCookies') {
    chrome.cookies.getAll({}, (cookies) => sendResponse({ cookies }));
    return true;
  }
});

攻撃者側(拡張が取り込むはずの「ユーザーコメント」を偽装)

<div id="comment">
  <!-- src="x"のように実在しない画像 URL を指定すると読み込みが失敗し、onerror が必ず発火 ⇒ 埋め込んだ JS が実行される -->
  <img src="x" onerror="window.postMessage({cmd:'dumpCookies'}, '*')" />
</div>

攻撃フロー

  1. content.js が 外部入力を HTML として描画 してしまう。ここを攻撃者は変えられないが、脆弱性を“用意”してくれている状態。
  2. 投稿欄などに悪意 HTML(②)を投入。ページを開いたユーザーのブラウザで content.js がその HTML を DOM へ挿入 → コンテンツスクリプトの権限で JS が実行。
  3. 実行された JS(window.postMessage({cmd:'dumpCookies'}))は、同じコンテキストにいる window.addEventListener('message', …) を経由して chrome.runtime.sendMessage に到達し、バックグラウンドが特権 API を実行する。
  4. background.js が “正規コマンド”と誤認して Cookie を返す。

対策としてまず大事なものは innerHTML や insertAdjacentHTML を使わないことです。もし使うとしても DOMPurify などサニタイザを必ず通すこと。
ページ ↔ 拡張間の通信では event.origin をチェックすることも 1 つの手になるかもしれません。

参考リンク

GitHubで編集を提案

Discussion