浏览器扩展权限审计,是终端安全里最容易被跳过的一环。绝大多数企业管住了系统补丁、管住了管理员权限,却对本机 Chrome 里装了什么扩展一无所知。一个申请了 debugger、cookies 和全站主机访问权的扩展,能力等价于在浏览器进程里放了一个常驻调试器:它能读走所有站点的登录态,能改写页面里的转账账号,也能把 Cookie 直接外传。2026 年多起浏览器扩展被收购后静默转向的事件说明,扩展的”当时无害”不代表”长期可信”。
适用场景
以下三类环境最需要做这件事:
- 办公终端统一管理:员工自行安装扩展,IT 没有清单,出了问题无法回溯是哪个扩展导致的。
- 财务、运营等敏感岗位:这些岗位日常登录后台系统,Cookie 价值高,是扩展类窃密的首要目标。
- 发生过账号异常但查不到原因:日志里只有正常的登录记录,没有暴力破解痕迹,这时候要怀疑浏览器侧。
不建议把这件事做成”一次性清查”。扩展会自动更新,权限会在新版本里扩张,审计必须是可重复执行的流程,而不是一份 Excel。
前置条件
- Chrome 或 Edge 版本不低于 96,ExtensionSettings 策略在该版本起完整支持
runtime_blocked_hosts。 - Windows 终端需有域策略或本地管理员权限写
HKLM;Linux 终端需 root。 - 一台可以运行 Python 3.8 以上版本的分析机,用于批量打分。
- 已有一份扩展白名单的业务依据,否则技术做完仍然无法判断”该不该封”。
原理说明:扩展权限到底能做什么
扩展的能力边界由 manifest.json 声明,浏览器在安装时一次性授权,之后扩展可以在不再次询问用户的情况下使用这些能力。
三类关键权限
- permissions:API 级能力。
cookies可读所有站点 Cookie,debugger可附加到标签页执行任意脚本,proxy可改全局代理设置把流量导向自建服务器,nativeMessaging可与本机程序通信,等于突破浏览器沙箱。 - host_permissions:站点范围。写成
<all_urls>意味着对用户访问的每一个站点都有读写 DOM 的能力,风险等级比任何单个 API 权限都高。 - content_scripts:注入脚本。即使没有 host_permissions,只要 matches 覆盖目标站点,同样能读取页面内容并回传。
策略的优先级
企业下发的 ExtensionSettings 优先级高于用户操作。策略里 installation_mode 设为 blocked 的扩展无法安装,已安装的会被禁用;设为 force_installed 的扩展用户无法卸载。这一点很关键:光靠通知员工卸载是无效的,必须走策略。
操作步骤
第一步:清点终端已安装扩展
Chrome 与 Edge 的扩展元数据都落在用户配置目录下,直接读文件比翻界面可靠。以下脚本在终端本机执行,输出一份 CSV 清单:
$paths = @(
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Extensions",
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Extensions"
)
$rows = foreach ($p in $paths) {
if (-not (Test-Path $p)) { continue }
Get-ChildItem $p -Directory | ForEach-Object {
$extId = $_.Name
Get-ChildItem $_.FullName -Directory | ForEach-Object {
$mf = Join-Path $_.FullName "manifest.json"
if (-not (Test-Path $mf)) { return }
$j = Get-Content $mf -Raw -Encoding UTF8 | ConvertFrom-Json
[PSCustomObject]@{
ExtId = $extId
Name = $j.name
Version = $j.version
Perms = ($j.permissions -join ",")
Hosts = ($j.host_permissions -join ",")
Source = $p
}
}
}
}
$rows | Export-Csv -NoTypeInformation -Encoding UTF8 ext-inventory.csv
"共采集 $($rows.Count) 条扩展记录"
输出字段里 Perms 是 API 权限,Hosts 是主机权限。如果某条记录的 Hosts 为空但扩展仍能读取页面,说明它用的是 content_scripts,需要额外看 manifest 里的注入配置。
第二步:按权限打分排序
几十台终端、上百个扩展,靠人眼看不过来,先用权重模型把高危的顶到前面:
import csv
# 权限风险权重:数值越高,越接近"可完全接管浏览器"
WEIGHT = {
"debugger": 5, "management": 5, "proxy": 4, "nativeMessaging": 4,
"scripting": 4, "cookies": 3, "clipboardRead": 3, "webRequest": 3,
"declarativeNetRequest": 3, "history": 2, "downloads": 2,
"tabs": 1, "storage": 0,
}
rows = []
with open("ext-inventory.csv", encoding="utf-8-sig") as f:
for r in csv.DictReader(f):
perms = [p.strip() for p in (r["Perms"] or "").split(",") if p.strip()]
hosts = (r["Hosts"] or "").strip()
score = sum(WEIGHT.get(p, 0) for p in perms)
if hosts in ("<all_urls>", "*://*/*"):
score += 5
elif hosts:
score += 2
rows.append((score, r["ExtId"], r["Name"][:26], ",".join(perms)[:56]))
rows.sort(reverse=True)
print(f"{'分':<4}{'扩展ID':<34}{'名称':<28}权限")
for s, i, n, p in rows[:25]:
print(f"{s:<4}{i:<34}{n:<28}{p}")
print(f"\n总分大于 8 的扩展:{sum(1 for r in rows if r[0] > 8)} 个")
打分不是目的,排序才是。经验上总分大于 8 的扩展应当逐个确认来源,总分大于 12 且不在白名单里的,直接进封禁候选。
第三步:下发 ExtensionSettings 策略
策略文件是一段 JSON,默认放行、点名强制、点名封禁:
{
"ExtensionSettings": {
"*": {
"installation_mode": "allowed",
"blocked_install_message": "该扩展未通过安全评审,请向 IT 提交申请单。"
},
"aeblfdkhhhdcdjpifhhbdiojplfjncoa": {
"installation_mode": "force_installed",
"update_url": "https://clients2.google.com/service/update2/crx"
},
"fdpohaocaechififmbbbbbknoalclacl": {
"installation_mode": "blocked"
}
}
}
通配符 * 的 allowed 表示用户仍可自行安装其他扩展,只是会看到提示语。如果要做到”只能装白名单”,把通配符改成 blocked,再逐个列出需要放行的 ID。后一种更严格,但对员工体验冲击大,建议先跑一个月 allowed 观察申请量。
Windows 端写入注册表,注意字典类型的策略在注册表里以 JSON 字符串形式存放:
$json = Get-Content .\chrome-extensions.json -Raw -Encoding UTF8
$key = "HKLM:\SOFTWARE\Policies\Google\Chrome"
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name "ExtensionSettings" -Value $json -Type String
Set-ItemProperty -Path $key -Name "ExtensionInstallAllowlist" -Value @() -Type MultiString
# Edge 同一套策略对象,只换注册表根
$keyEdge = "HKLM:\SOFTWARE\Policies\Microsoft\Edge"
New-Item -Path $keyEdge -Force | Out-Null
Set-ItemProperty -Path $keyEdge -Name "ExtensionSettings" -Value $json -Type String
gpupdate /force | Out-Null
"策略已写入,终端刷新 chrome://policy 后生效"
Linux 端更简单,策略目录放文件即可,Chrome 为 /etc/opt/chrome/policies/managed/,Edge 为 /etc/opt/edge/policies/managed/,文件名任意,后缀必须是 .json。
第四步:收敛运行时主机访问范围
有些扩展业务上确实需要放行,但不需要访问全部站点。这时用运行时主机限制把它圈在允许的域内:
{
"ExtensionSettings": {
"aeblfdkhhhdcdjpifhhbdiojplfjncoa": {
"installation_mode": "force_installed",
"runtime_blocked_hosts": [
"*://*.internal.corp.example",
"*://10.20.0.0/16",
"*://admin.example.com"
],
"runtime_allowed_hosts": ["*://wiki.example.com"]
}
}
}
runtime_blocked_hosts 的过滤发生在扩展发起请求时,命中即被浏览器拒绝,扩展侧拿不到任何响应内容。它不阻止扩展加载,只是切断它对指定主机的访问,因此适合”功能保留、范围收窄”的场景。
第五步:封禁与强制移除
blocked 只是禁用,扩展文件仍在磁盘上。要彻底移除,在策略对象里改用 removed:
{
"ExtensionSettings": {
"fdpohaocaechififmbbbbbknoalclacl": {
"installation_mode": "removed"
}
}
}
这两个模式的区别值得记住:blocked 保留扩展数据,日后解封可以恢复配置;removed 会清掉扩展的本地存储,属于不可逆操作。对确认的恶意扩展用 removed,对”暂不确定”的用 blocked。
配置验证
策略下发后必须验证三件事:文件语法正确、浏览器真的读到了、运行时确实生效。
# 1) 策略文件语法校验
jq . /etc/opt/chrome/policies/managed/extensions.json > /dev/null && echo "JSON OK"
# 2) 确认策略文件被 Chrome 读取(策略目录必须归属 root 且不可写)
stat -c "%U:%G %a %n" /etc/opt/chrome/policies/managed/extensions.json
# 期望输出:root:root 644
# 3) 终端侧核对:打开 chrome://policy,点"重新加载政策"
# ExtensionSettings 行状态应为 OK,且不出现在"未应用"列表
浏览器侧还要确认策略没有被用户配置文件覆盖。在地址栏输入 chrome://policy,展开 ExtensionSettings,状态为 OK 且”来源”显示为平台策略,才算真正生效。如果显示 Not applied,通常是 JSON 语法错、策略目录权限过宽,或注册表键名拼写错误。
最后做一次周期性比对,把”装了但不在名单里”的扩展捞出来:
# 从策略里取出已批准扩展 ID 与安装模式
jq -r '.ExtensionSettings | to_entries[] | [.key, (.value.installation_mode // "allowed")] | @tsv' \
/etc/opt/chrome/policies/managed/extensions.json \
| sort > approved.tsv
# 从终端清单里取出实际安装的扩展 ID
tail -n +2 ext-inventory.csv | cut -d, -f1 | sort -u > installed.tsv
# 差集:装了但不在策略名单里的(需人工判定是允许还是封禁)
comm -23 installed.tsv approved.tsv
这个比对建议做成每周任务,输出直接进安全运营群。扩展自动更新后重新申请权限的情况,就靠这个差集发现。
常见问题
Q1:策略下发后扩展还在,只是显示”由贵单位管理”,是不是没生效?
这是生效的正常表现。Chrome 对被策略管理的扩展会显示”由贵单位管理”标签,扩展仍然加载,但用户无法卸载、无法关闭。force_installed 的设计目的就是”必须存在”,所以它不会消失。要确认是否真的受控,看 chrome://policy 的状态列,而不是看扩展是否还在列表里。
Q2:runtime_blocked_hosts 配置了,扩展照样能读到页面内容,为什么?
大概率是扩展通过 content_scripts 直接注入,而不是走网络请求。runtime_blocked_hosts 只拦截扩展发起的请求,管不到内容脚本在页面上下文里的执行。对于这类扩展,只能靠 host_permissions 层面的策略收窄,或者干脆封禁。判断方法:在 chrome://extensions 打开开发者模式,看该扩展的”检查视图”是否在目标页面上存在。
Q3:员工用绿色版或便携版浏览器,策略还能管住吗?
不能。便携版浏览器通常不读系统策略目录,注册表策略也可能被绕过。这类情况要从终端管控层解决:应用白名单只允许安装受管版本,或者用 AppLocker、WDAC 限制可执行文件路径。策略是浏览器层的约束,前提是浏览器本身受管。
总结
浏览器扩展权限审计的核心不是写一条策略,而是建立”清点—打分—下发—比对”的闭环。清点解决看不见的问题,打分解决优先级问题,策略解决执行力问题,比对解决扩展更新后权限悄悄扩张的问题。四步里最容易漏掉的是最后一步:扩展不是静态资产,它会自己变,审计也必须持续运行。对中小企业来说,先做一份全量清单,把 <all_urls> 加 cookies 的组合挑出来处理,收益已经足够覆盖成本。