環境情報
| 項目 | バージョン / 内容 |
|---|---|
| OS | macOS 14.5 / Ubuntu 22.04 LTS |
| Chrome | 124〜126(Enterprise 管理下) |
| Node.js | 20.14.x |
| 使用ツール |
curl, python3(3.11+), jq, ripgrep
|
| 検証対象 | Chrome Web Store で配布中の任意の拡張(MV2 / MV3) |
結論から書くと、Chrome Web Store の掲載ページだけでは拡張の安全性は判断できません。権限の詳細は要約表示に丸められ、「外部にデータを送っているか」はストア上からは一切見えません。そこで本記事では、配布中の CRX バイナリを直接取得して展開し、manifest.json の権限が最小構成か、全 JS に fetch / XMLHttpRequest / WebSocket / sendBeacon が存在しないかを機械的に実査する手順をまとめます。
なぜストアの掲載情報では足りないのか
- ストアが表示する権限は人間向けに要約された文言で、
manifest.jsonの生のpermissions/host_permissionsとは粒度が違う -
optional_permissionsは「実行時に要求される」ため掲載情報にほぼ出てこない - コードは minify / bundle 済みで、ストア上では中身を読めない
- 自動更新が既定で有効。今見ているバージョンの挙動は、翌週には保証されない
つまり「ストアの審査を通っているから安全」は根拠になりません。審査はポリシー違反の検出であって、あなたの環境でのデータ送信の有無を保証するものではありません。
手順1: 拡張IDを特定して CRX を取得する
拡張IDはストア URL の末尾 32 文字です。
https://chromewebstore.google.com/detail/<name>/<EXTENSION_ID>
CRX は Google の update エンドポイントから直接取得できます。
EXTENSION_ID="abcdefghijklmnopabcdefghijklmnop"
curl -sL -o ext.crx \
"https://clients2.google.com/service/update2/crx?response=redirect&prodversion=124.0&acceptformat=crx2,crx3&x=id%3D${EXTENSION_ID}%26uc"
ls -l ext.crx
手順2: CRX を展開する(CRX3 は unzip 不可)
CRX3 は ZIP の先頭にヘッダが付くため、そのまま unzip すると失敗します。ヘッダ長を読んでオフセットを飛ばします。
# unpack_crx.py: CRX2/CRX3 のヘッダを読み飛ばして ZIP 部分だけを展開する
import io, struct, sys, zipfile
src, dst = sys.argv[1], sys.argv[2]
data = open(src, "rb").read()
assert data[:4] == b"Cr24", "not a CRX file"
version = struct.unpack("<I", data[4:8])[0]
if version == 3:
header_size = struct.unpack("<I", data[8:12])[0]
offset = 12 + header_size
elif version == 2:
pubkey_len = struct.unpack("<I", data[8:12])[0]
sig_len = struct.unpack("<I", data[12:16])[0]
offset = 16 + pubkey_len + sig_len
else:
raise SystemExit(f"unsupported CRX version: {version}")
with zipfile.ZipFile(io.BytesIO(data[offset:])) as z:
z.extractall(dst)
print("extracted:", dst, "files:", len(z.namelist()))
python3 unpack_crx.py ext.crx ./unpacked
find ./unpacked -maxdepth 2 -type f | head -50
手順3: manifest.json の権限を確認する
まず宣言された権限を全部出します。「要約」ではなく生の値を見るのが重要です。
jq '{
manifest_version, version, update_url,
permissions, optional_permissions, host_permissions,
optional_host_permissions, externally_connectable,
content_scripts: [.content_scripts[]? | {matches, js, run_at}],
background, web_accessible_resources
}' ./unpacked/manifest.json
権限の読み方はおおむね次の表のとおりです。
| permission | できること | 最小構成と言えるか |
|---|---|---|
activeTab |
ユーザー操作時のみ現在タブへアクセス | ○ 最小に近い |
storage |
拡張ローカル保存 | ○ 用途次第で妥当 |
tabs |
URL・タイトル等の取得 | △ 本当に必要か要確認 |
cookies |
Cookie の読み書き | ✕ 強い権限。正当化必須 |
webRequest / declarativeNetRequest
|
通信の観測・改変 | ✕ 全通信が見える |
<all_urls> / *://*/*
|
全サイトの DOM と通信 | ✕ 原則として過剰 |
clipboardRead |
クリップボード読取 | ✕ 機密漏えい経路 |
<all_urls> が入っているのに、実際の content_scripts.matches が 1 ドメインしかない、といった権限と実装の乖離は典型的な赤信号です。
手順4: 外部送信APIを grep で実査する
宣言ではなく実装を見ます。ネットワーク系 API を網羅的に検索します。
rg -n --hidden -g '!**/*.map' -g '!**/*.min.js' \
-e 'fetch\s*\(' \
-e 'XMLHttpRequest' \
-e 'new\s+WebSocket' \
-e 'sendBeacon' \
-e 'EventSource' \
-e 'navigator\.connection' \
./unpacked
続けて、難読化・動的実行の痕跡も見ます。ここが空なら「素直な実装」の可能性が上がります。
rg -n -e 'eval\s*\(' -e 'new\s+Function' -e 'atob\s*\(' \
-e 'fromCharCode' -e 'chrome\.runtime\.getURL' \
-e 'String\.raw' ./unpacked
atob と eval の組み合わせ、長大な 16 進文字列、.wasm の同梱などが出てきたら、静的 grep だけでは判断を諦めて動的解析(DevTools の Network タブ + chrome.debugger)に切り替えるべきです。
手順5: バージョン固定と差分再確認を運用に組み込む
静的確認は確認したバージョン限りです。自動更新で挙動は変わります。そこで取得から差分検出までをスクリプト化して定期実行します。
#!/usr/bin/env bash
# audit_crx.sh: 取得 → 展開 → 権限抽出 → 通信API抽出 → 前回との diff
set -euo pipefail
ID="abcdefghijklmnopabcdefghijklmnop"
BASE="audit/${ID}"
mkdir -p "${BASE}/current"
curl -sL -o "${BASE}/current/ext.crx" \
"https://clients2.google.com/service/update2/crx?response=redirect&prodversion=124.0&acceptformat=crx2,crx3&x=id%3D${ID}%26uc"
rm -rf "${BASE}/current/unpacked"
python3 unpack_crx.py "${BASE}/current/ext.crx" "${BASE}/current/unpacked"
jq -S '{permissions, optional_permissions, host_permissions}' \
"${BASE}/current/unpacked/manifest.json" > "${BASE}/current/perms.json"
rg -n -e 'fetch\s*\(' -e 'XMLHttpRequest' -e 'WebSocket' -e 'sendBeacon' \
"${BASE}/current/unpacked" > "${BASE}/current/net.txt" || true
for f in perms.json net.txt; do
if [ -f "${BASE}/prev_${f}" ]; then
diff -u "${BASE}/prev_${f}" "${BASE}/current/${f}" || true
fi
cp "${BASE}/current/${f}" "${BASE}/prev_${f}"
done
これを GitHub Actions の cron に載せれば、権限追加や通信コードの混入を Pull Request 的に検知できます。
name: crx-audit
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y ripgrep jq
- run: ./scripts/audit_crx.sh
重要な端末では、Chrome のエンタープライズポリシー ExtensionSettings で installation_mode を固定し、update_url を自前管理に切り替えるのが確実です。少なくとも「更新後に差分を再確認する」ところまでをセットにしないと、確認した意味が数週間で消えます。
FAQ
Q. ストアの権限表示と manifest.json が違って見えるのはなぜ?
A. ストアは要約を表示しており、optional_permissions や host_permissions の細かいパターンは省略されます。判断は必ず生の manifest で行ってください。
Q. fetch が grep で 0 件なら安全と言える?
A. 言えません。文字列連結(window['fe'+'tch'])、テンプレートリテラル、WASM バイナリ内、リモート設定で後から有効化される経路は grep をすり抜けます。「見つからなかった」は「そのバージョンの平文 JS には無かった」と読み替えるのが正確です。
Q. 自動更新を止めれば確認は一度で済む?
A. 済みません。更新を止めても依存する外部 API やリモート設定は変わります。また MV2 の廃止スケジュールのように、放置が別のリスクになる場合もあります。
Q. MV3 の Service Worker はどう確認する?
A. chrome://extensions の「Service Worker」リンクから DevTools を開き、Network タブを有効化したまま操作します。バックグラウンドの fetch はここに出ます。静的解析と動的観測は併用が前提です。
まとめ
- ストア掲載情報は要約であり、権限の詳細も外部送信の有無も判断できない
- CRX を直接取得 → ヘッダを飛ばして展開 →
manifest.jsonと全 JS を実査する - 権限は「宣言」と「matches / 実装」の乖離を見る。
<all_urls>とcookiesは特に疑う - 通信 API と難読化 API を grep し、動的解析に切り替える基準を先に決めておく
- 静的確認はバージョン限り。差分再確認までを運用にセットにする
あなたは拡張の安全性を、どこまで確認していますか?
この記事を書いた人
BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。
GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。
👉 BENTEN Web Works — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中