0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chrome拡張の安全性はストア掲載情報では判断できない — CRXを展開して権限と外部送信を実査する手順

0
Posted at

環境情報

項目 バージョン / 内容
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

atobeval の組み合わせ、長大な 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 のエンタープライズポリシー ExtensionSettingsinstallation_mode を固定し、update_url を自前管理に切り替えるのが確実です。少なくとも「更新後に差分を再確認する」ところまでをセットにしないと、確認した意味が数週間で消えます。

FAQ

Q. ストアの権限表示と manifest.json が違って見えるのはなぜ?
A. ストアは要約を表示しており、optional_permissionshost_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) — 日々の知見を発信中

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?