はじめに
Chrome拡張機能で任意のWebページからメタ情報を取得するブックマークツールを開発した際、content_scriptsのmatchesに"https://*/*"を指定したところ、「権限が広すぎる(broad permissions)」という指摘を受けました。
「任意のページ情報を取得するのだから全サイト許可は妥当」と考えていたのですが、調べていくうちに、content_scriptsの常時注入という仕組み自体が、不特定多数のサイトを対象とする場合にはセキュリティ上の懸念となることがわか離ました。
この記事では、content_scriptsのリスクと、Manifest V3で推奨されているscripting APIを使った解決策についてまとめました。
最初にまとめ
Chrome拡張でページの情報を取得する方法は3つある。
- content_scripts — ページにスクリプトを自動注入してDOMから取得
- scripting API — 関数を一時的に注入してDOMから取得
- chrome.tabs API — スクリプト注入なしで、ブラウザが管理する情報(URL、タイトルなど)を直接取得
content_scriptsのリスク:
matches(スクリプトを注入する対象ページのURLパターン)を広く設定しすぎると、無分別にスクリプトを注入する設計になってしまう。
解決策:
対象サイトが不特定の場合は、content_scriptsを使わず、activeTab権限とscripting APIを組み合わせて「ユーザーが操作を行った時だけ」スクリプトを注入する設計にする。
1. Chrome拡張の基本設計:プロセスの分離
Chrome拡張機能は、主に2つの異なるプロセスで役割を分担している。この前提の理解が、権限設計の話に必須となる。
Service Worker(background.js)
ブラウザのバックグラウンドで動くプロセス。API通信やイベントの監視を行うが、WebページのDOMには直接アクセスできない(documentが存在しない環境)。
Content Scripts(content.js)
Webページ内で動くプロセス。ページのコンテキストで動作するため、DOMの読み書きが直接行える。
参考: Content scripts — Chrome for Developers
2. content_scriptsの注入の仕組みとリスク
content_scriptsは、manifest.jsonのmatchesで指定したURLパターンに一致するページが開かれた瞬間に、自動的にスクリプトを注入する仕組みだ。
対象が特定されている場合
特定のブログサービス専用ツールなど、注入する対象が限定されている場合は合理的な設計となる。
"content_scripts": [
{
"matches": ["https://specific-blog.com/*"],
"js": ["content.js"]
}
]
対象が不特定の場合
ブックマークツールのようにあらゆるサイトを対象とする場合、以下のように広く指定せざるを得ない。
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"]
}
]
しかし、この設定ではオンラインバンキングや機密情報を扱う社内システムなどを開いた際にも無条件にスクリプトが注入される。万が一拡張機能が乗っ取られた場合、全サイトが攻撃対象になりうる。
Chrome Web Storeでは、このような設定は「broad permissions」としてフラグが立ち、審査が厳格になる。
参考: Chromium Extensions Group — Clarification about activeTab and tabs permissions
3. 解決策:scripting APIとactiveTabの活用
scripting API
Manifest V3では、content_scriptsのセキュリティ上の弱点に対応するためにscripting APIが導入された。
Service Worker自体はDOMにアクセスできないが、chrome.scripting.executeScript()を使うことで、関数をページ側に送り込んで実行させることができる。
activeTab
activeTabは、ユーザーが拡張機能のアイコンをクリックするなどの明示的なアクションを起こした瞬間にだけ、現在のタブへの一時的なアクセス権を付与する権限。タブの切り替えなどの自動イベントには反応しない。
activeTab + scripting APIの組み合わせ
この2つを組み合わせることで、「ユーザーが操作した時だけ、そのタブにだけ」スクリプトを注入できる。最小限の権限でDOMの情報を取得する設計になる。
// background.js(Service Worker)
chrome.action.onClicked.addListener((tab) => {
// ユーザーがクリックした時だけ、対象タブに関数を注入
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: getMetaData
}, (results) => {
const metadata = results[0].result;
console.log("取得したメタデータ:", metadata);
});
});
// ページ側で実行される関数(content.jsと同じ立場)
function getMetaData() {
return {
title: document.title,
url: window.location.href,
description: document.querySelector('meta[name="description"]')?.content || ""
};
}
これにより、manifest.jsonからcontent_scriptsを完全に外せる。broad matchesの問題自体がなくなる。
参考: chrome.scripting — Chrome for Developers
注意:default_popupとの競合
chrome.action.onClickedは、popupが設定されていない場合にのみ発火する。popupを使う場合は、popup内のJavaScriptからchrome.scripting.executeScript()を直接呼ぶか、chrome.runtime.sendMessage()でService Workerに処理を委譲する。
参考: Inject scripts into the active tab (Tutorial) — Chrome for Developers
4. スクリプト注入が不要なケース:chrome.tabs API
ここまでの話は「DOMにアクセスする必要がある場合」の選択肢だった。しかし、そもそもスクリプトを注入せずに情報を取れるケースもある。
chrome.tabs.get()やchrome.tabs.query()は、ブラウザが内部的に管理しているタブの属性(URL、タイトル、favicon)を読み取るAPIで、ページにスクリプトを一切注入しない。ページ側からも検知できない。
// background.js(Service Worker内で完結、スクリプト注入なし)
chrome.tabs.onActivated.addListener(async (activeInfo) => {
const tab = await chrome.tabs.get(activeInfo.tabId);
// tab.url と tab.title はブラウザが管理している情報
// ページのDOMには一切触れていない
console.log(tab.url, tab.title);
});
URLやタイトルだけで足りるなら、スクリプト注入は不要。OGPタグやarticle:published_timeのようなページ固有の情報を読みたい場合にだけ、scripting APIやcontent_scriptsを使えばいい。
参考: chrome.tabs API — Chrome for Developers
5. tabs権限とactiveTab権限の違い
権限設定で指定するtabsとactiveTabは名前が似ているが、役割が明確に異なる。
tabs権限:
ユーザーの操作に関係なく、常にすべてのタブのURLやタイトルを読み取れる権限。chrome.tabs.onActivatedなどの自動発火イベントを使う場合に必要。
activeTab権限:
ユーザーが拡張機能を呼び出した瞬間にだけ、現在のタブに対して付与される一時的な権限。権限の範囲が狭いため、まずactiveTabで足りるかを検討し、自動イベントが必要な場合にのみtabsを使うのが望ましい。
参考: chrome.tabs API — Chrome for Developers
6. 使い分けの指針
content_scriptsとscripting APIは排他的な関係ではなく、1つの拡張内で併用もできる。
content_scriptsが適切な場面:
対象サイトが明確で、matchesを狭く絞れる場合。例えばmatches: ["https://specific-service.com/*"]のような特定サービス専用の拡張。
scripting APIが適切な場面:
対象サイトが不特定で、matchesを広くせざるを得ない場合。ブックマークツールのようなケース。
chrome.tabs APIで十分な場面:
URLやタイトルなど、ブラウザが管理している情報だけで足りる場合。スクリプト注入自体が不要。
おまけ:manifest.jsonの例
ブックマークツールで「タブ切替時にDBで登録済みチェック」と「ユーザー操作時にメタデータ取得」の2機能を持つ場合の設定例。
{
"name": "Bookmark Extension",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_title": "ブックマークする",
"default_popup": "popup.html"
},
"permissions": [
"tabs",
"activeTab",
"scripting"
],
"host_permissions": [
"http://localhost:3000/*",
"https://bxxxxxx.jp/*"
],
"background": {
"service_worker": "background.js"
}
}
tabs— タブ切替時の自動イベント(onActivated)でURLを取得するために必要activeTab— ユーザーがクリックした時にだけ、そのタブへの一時的なアクセス権を付与scripting—executeScript()でページにスクリプトを注入するために必要host_permissions— データ送信先のAPIエンドポイントのみに限定
content_scriptsが存在しない点がポイント。broad matchesの問題自体が発生しない。
参考リンク一覧
- The “activeTab” permission — Chrome for Developers
- Declare permissions — Chrome for Developers
- Inject scripts into the active tab (Tutorial) — Chrome for Developers
- Permissions list — Chrome for Developers
- chrome.tabs API — Chrome for Developers
- chrome.scripting — Chrome for Developers
- Content scripts — Chrome for Developers
- Chromium Extensions Group — Clarification about activeTab and tabs permissions
Photo by Firmbee.com on Unsplash

