Vercel 被黑全过程拆解——一个 AI 工具怎么变成供应链攻击入口
4月19号,Vercel 发了一份安全公告,承认内部系统被未授权访问。CEO Guillermo Rauch 同一天在 X 上发了长贴,点名说攻击起点是一个叫 Context.ai 的第三方 AI 工具。
这事的攻击链值得认真拆一遍。不是因为手法多新——OAuth 滥用和供应链攻击都是老套路了。值得拆是因为从入口到最终拿到客户环境变量,中间每一步都利用了我们日常开发中"默认信任"的环节。
攻击发生了什么
先理清时间线。这不是一次突击行动,而是跨了差不多 22 个月的持久渗透:
- 2024年6月左右:Context.ai 的 Google Workspace OAuth 应用被攻破
- 2024下半年到2025年初:攻击者通过 OAuth token 维持持久访问
- 2025年初:从 Context.ai 的 OAuth 权限横向移动到一名 Vercel 员工的 Google Workspace 账户
- 2025年中:进入 Vercel 内部系统,开始遍历客户环境变量
- 2026年4月10日:有客户收到 OpenAI 的 API key 泄露通知,key 只存在于 Vercel 的环境变量里
- 2026年4月19日:Vercel 正式披露
22 个月的驻留时间。Google Workspace 的 OAuth 审计日志默认只保留 6 个月,意味着最早期的入侵活动日志在调查开始前就已经过期了。
攻击链五个阶段
第一步:拿下第三方 OAuth 应用
Context.ai 是一个 AI 分析工具,它有一个 Google Workspace OAuth 应用,Vercel 的员工授权过这个应用。攻击者拿下了这个 OAuth 应用——具体怎么拿下的 Context.ai 那边没有公开披露。
Vercel 在公告里给出了被攻破的 OAuth 应用 ID:
110671459871-30f1spbu0hptbs60cb4vsmv79i7bbvqj.apps.googleusercontent.com
OAuth 应用一旦被授权,产生的 token 有几个特性让它成了攻击者的理想切入点:
- 不需要用户密码就能用
- 用户改密码对它没影响,token 照常有效
- scope 通常很宽(邮件、云盘、日历都能访问)
- 授权之后几乎没人会再审计它
我写了一个脚本,可以查你的 Google Workspace 里有哪些第三方 OAuth 应用还活着:
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
def list_third_party_apps(admin_creds_path: str, customer_id: str = "my_customer"):
"""列出 Workspace 中所有第三方 OAuth 应用授权"""
creds = Credentials.from_authorized_user_file(admin_creds_path)
service = build("admin", "reports_v1", credentials=creds)
# 拉 OAuth token 授权事件
results = service.activities().list(
userKey="all",
applicationName="token",
eventName="authorize",
maxResults=500,
customerId=customer_id,
).execute()
apps = {}
for activity in results.get("items", []):
for event in activity.get("events", []):
params = {p["name"]: p.get("value", p.get("multiValue", ""))
for p in event.get("parameters", [])}
client_id = params.get("client_id", "unknown")
app_name = params.get("app_name", "unknown")
scopes = params.get("scope", "")
user = activity.get("actor", {}).get("email", "unknown")
if client_id not in apps:
apps[client_id] = {
"name": app_name,
"users": set(),
"scopes": set(),
}
apps[client_id]["users"].add(user)
if isinstance(scopes, list):
apps[client_id]["scopes"].update(scopes)
else:
apps[client_id]["scopes"].add(scopes)
# 重点关注的高危 scope
dangerous_scopes = {
"https://mail.google.com/",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/admin.directory.user",
}
print(f"\n共发现 {len(apps)} 个第三方 OAuth 应用\n")
for cid, info in apps.items():
risky = info["scopes"] & dangerous_scopes
flag = " ⚠️ 高危" if risky else ""
print(f"[{info['name']}]{flag}")
print(f" Client ID: {cid}")
print(f" 授权用户数: {len(info['users'])}")
print(f" Scopes: {', '.join(info['scopes'])}")
if risky:
print(f" 高危 scope: {', '.join(risky)}")
print()
if __name__ == "__main__":
list_third_party_apps("admin_creds.json")
跑一遍你大概率会发现几个早就不用了但 token 还活着的应用。
第二步:从 OAuth 横移到员工 Workspace 账户
拿到了 Context.ai 的 OAuth 权限后,攻击者用这个权限访问了 Vercel 一名员工的 Google Workspace 账户。这意味着攻击者能看这个员工的邮件、Google Drive 文档、日历里的会议和链接。
Rauch 在声明里用的原话是"a series of maneuvers that escalated from our colleague's compromised Vercel Google Workspace account"。具体是通过 SSO 联合认证跳过去的,还是从邮件/文档里收割到了更多凭证,Vercel 没有公开说。
这里有一个设计层面的问题:Google Workspace OAuth 应用授权后产生的 token,在用户改密码、开 2FA 之后仍然有效。想撤销得去 Workspace Admin Console 手动操作。
检查你 Workspace 账户当前活跃的 OAuth token:
# Google Workspace Admin SDK - 列出指定用户的 OAuth token
curl -s "https://admin.googleapis.com/admin/directory/v1/users/{user_email}/tokens" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" | \
python3 -c "
import json, sys
data = json.load(sys.stdin)
for token in data.get('items', []):
print(f\"App: {token.get('displayText', 'unknown')}\")
print(f\" Client ID: {token['clientId']}\")
print(f\" Scopes: {', '.join(token.get('scopes', []))}\")
print(f\" User: {token.get('userKey', 'unknown')}\")
print()
"
第三步:进入 Vercel 内部系统
有了员工的 Workspace 账户权限,攻击者进入了 Vercel 的内部系统。Rauch 公开说攻击者的行动速度极快,对 Vercel 内部架构的了解程度很深,推测使用了 AI 辅助加速。
这也是 2026 年攻击趋势的一个缩影——攻击者用 AI 来加速漏洞利用和内部侦查,不是什么赛博朋克概念了,已经在真实事件中出现。
第四步:遍历环境变量
这一步是整个事件伤害最大的环节。
Vercel 的环境变量有一个"sensitive"标记机制。标记为 sensitive 的变量加密存储,内部系统也没法直接读取明文。但标记为"non-sensitive"的变量——虽然也是加密存储的——在内部系统有权限的情况下可以解密读取。
问题在于,在这次事件之前,Vercel 创建环境变量时默认不是 sensitive 的。很多团队在设置 DATABASE_URL、API_KEY、各种 token 的时候,根本没注意到这个选项。
事后 Vercel 的修复措施之一就是把默认值改成了 sensitive: on。
这个设计给我们的教训很直接:安全敏感的配置项,默认值必须是最严格的那个。让用户主动选择降低安全性(opt-out),而不是让用户记住要手动提高安全性(opt-in)。
第五步:下游影响
泄露的环境变量里包含各种下游服务的凭证——数据库连接串、OpenAI API key、第三方 SaaS 的 token。
有一个用户 Andrey Zagoruiko 在 4月10日——Vercel 正式披露前 9 天——就收到了 OpenAI 的通知,说他的 API key 被泄露了。而这个 key 只存在 Vercel 的环境变量里。
这说明攻击者不只是读取了这些凭证,还在实际使用它们。
你的项目该查什么
如果你在用 Vercel,下面这些事现在就该做:
1. 检查环境变量的 sensitive 标记
# 用 Vercel CLI 列出项目环境变量
vercel env ls --json | python3 -c "
import json, sys
envs = json.load(sys.stdin)
non_sensitive = [e for e in envs if e.get('type') != 'sensitive']
if non_sensitive:
print(f'⚠️ 发现 {len(non_sensitive)} 个非 sensitive 环境变量:')
for e in non_sensitive:
print(f\" {e['key']} (target: {e.get('target', 'unknown')})\")
else:
print('✅ 所有环境变量都标记为 sensitive')
"
2. 审计 OAuth 应用授权
重点看有没有这个 Client ID:
110671459871-30f1spbu0hptbs60cb4vsmv79i7bbvqj.apps.googleusercontent.com
即使没有中招,也建议清理一遍不再使用的 OAuth 授权。
3. 检查 CI/CD 中的 Action 是否 pin 了 SHA
# 有风险——tag 可以被覆盖指向恶意代码
- uses: some-ai-vendor/vercel-deploy-action@v2
# 安全——SHA 是不可变的
- uses: some-ai-vendor/vercel-deploy-action@a3f8c1d2e4b5f6a7b8c9d0e1f2a3b4c5d6e7f8a9
4. Webhook 接收端要验签
如果你有 Vercel Webhook 触发的自动化流程,确认处理请求之前验证了 x-vercel-signature:
import crypto from "crypto";
function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac("sha1", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// 在处理请求的入口
export async function POST(req: Request) {
const rawBody = await req.text();
const sig = req.headers.get("x-vercel-signature") || "";
if (!verifyWebhook(rawBody, sig, process.env.WEBHOOK_SECRET!)) {
return new Response("Unauthorized", { status: 401 });
}
const event = JSON.parse(rawBody);
// 签名验证通过,继续处理
}
5. MCP/Agent 工具配置不要转发高权限 token
如果你在用 MCP 或者其他 Agent 工具框架,检查配置里有没有把 VERCEL_TOKEN 之类的高权限 token 传给第三方 server:
// ❌ 高风险——token 传给了第三方
{
"servers": {
"vercel-deployer": {
"url": "https://mcp.some-vendor.com/vercel",
"env": {
"VERCEL_TOKEN": "${VERCEL_TOKEN}"
}
}
}
}
// ✅ 用 scope 最小的专用 token + 本地 server
{
"servers": {
"vercel-deployer": {
"command": "npx",
"args": ["@vercel/mcp-server"],
"env": {
"VERCEL_TOKEN": "${VERCEL_DEPLOY_ONLY_TOKEN}"
}
}
}
}
2026 年供应链攻击的规律
这不是孤例。2026 年已经出了好几起同类事件:
- LiteLLM:PyPI 包被植入恶意代码,窃取 API key
- Axios:npm 包被投毒
- Codecov:CI 工具被攻破,窃取环境变量中的凭证
- CircleCI:内部系统被攻破,客户密钥泄露
看出规律了吗?攻击者不直接攻击目标公司。攻击目标公司信任的第三方工具、依赖库、CI/CD 服务。信任关系就是攻击面。
Vercel 的 CEO 在声明里说了一句值得记住的话:攻击者对 Vercel 系统的了解程度和行动速度,说明他们"很可能得到了 AI 的大幅加速"。
安全防御这边也得跟上。我在项目里加了一个定时脚本,每周扫一遍 OAuth 授权和环境变量状态:
#!/usr/bin/env python3
"""每周安全扫描:OAuth 应用 + 环境变量审计"""
import subprocess
import json
from datetime import datetime
def audit_vercel_env():
"""检查 Vercel 环境变量的 sensitive 状态"""
result = subprocess.run(
["vercel", "env", "ls", "--json"],
capture_output=True, text=True
)
if result.returncode != 0:
print("❌ Vercel CLI 执行失败")
return
envs = json.loads(result.stdout)
issues = []
for env in envs:
if env.get("type") != "sensitive":
issues.append(env["key"])
if issues:
print(f"⚠️ {len(issues)} 个环境变量未标记 sensitive:")
for key in issues:
print(f" - {key}")
else:
print("✅ 环境变量检查通过")
return issues
def audit_github_actions(repo_path: str = "."):
"""检查 GitHub Actions 中是否有未 pin SHA 的 action"""
import glob
import re
workflow_files = glob.glob(f"{repo_path}/.github/workflows/*.yml")
unpinned = []
sha_pattern = re.compile(r"uses:\s+[\w\-./]+@[a-f0-9]{40}")
tag_pattern = re.compile(r"uses:\s+([\w\-./]+@v[\d.]+)")
for wf in workflow_files:
with open(wf) as f:
for i, line in enumerate(f, 1):
if tag_pattern.search(line) and not sha_pattern.search(line):
match = tag_pattern.search(line)
unpinned.append(f"{wf}:{i} -> {match.group(1)}")
if unpinned:
print(f"\n⚠️ {len(unpinned)} 个 Action 未 pin SHA:")
for item in unpinned:
print(f" - {item}")
else:
print("✅ GitHub Actions 检查通过")
return unpinned
if __name__ == "__main__":
print(f"=== 安全审计 {datetime.now().strftime('%Y-%m-%d %H:%M')} ===\n")
audit_vercel_env()
audit_github_actions()
总结几个可操作的点
这次事件给开发团队的教训不复杂,但做不做差别很大:
- OAuth 应用当供应商管理。每季度审计一次,不用的立刻撤销。关注 scope 范围,邮件和 Drive 权限给 AI 工具之前想清楚。
- 环境变量全标 sensitive。Vercel 已经改了默认值,但其他 PaaS 平台(Netlify、Railway、Render)不一定。自己检查。
- CI/CD Action pin SHA。tag 可以被覆盖,SHA 不行。麻烦但有效。
- Webhook 验签。接收外部请求不验签就等于裸奔。
- Agent 工具配置最小权限。MCP server 用专用的、scope 最小的 token,别把 admin token 传出去。
- Google Workspace OAuth 审计日志只保留 6 个月。如果你的安全调查需要更长的回溯窗口,得自己做日志转储。