约翰霍普金斯大学研究团队通过在GitHub PR标题中嵌入指令,成功在完全不触碰Claude Code、Gemini CLI和Copilot的情况下窃取敏感信息。攻击利用了AI模型将工具返回内容视为可信这一天然信任倾向,无需恶意服务器即可完成注入。
2026年4月,一个约翰霍普金斯大学的研究团队劫持了Claude Code、Gemini CLI和GitHub Copilot,全程没有触碰任何一个工具。他们只是修改了一个GitHub Pull Request的标题。
这些Agent在执行它们的工作:读取PR上下文来理解需要审查的内容。隐藏在标题里的指令看起来就像模型看到的普通任务上下文。它照着做了。GitHub Actions的密钥就这样泄露了出去。
没有人写恶意软件。没有人弹一个shell。"漏洞"就是一句话。
人们听到"MCP安全"时,脑海里浮现的是一个由攻击者运行的恶意服务器。这很合理。这确实是一个真实的威胁类别,而且工具投毒(在工具描述中隐藏指令——模型读取而用户永远看不到的那部分)也已被记录在案,且在不断增长。
但约翰霍普金斯大学的这次劫持并不需要一个恶意服务器。它只需要一个良性、诚实的MCP集成——"获取这个PR的数据"——加上一个模型,仅仅因为数据是通过工具调用而非聊天消息返回的,就将其视为可信。在MCP中,注入不是来自用户,而是来自工具返回的任何内容,而且当它进入上下文时,模型已经无法区分"这是数据"和"这是你的下一个指令"。
2026年中期的多起高危漏洞披露补充了让问题变得代价高昂的那部分:Cursor、Claude Code、Gemini CLI、Copilot和Amazon Q都会用开发者自己的操作系统级权限自动执行项目定义的MCP服务器。没有沙箱。没有隔离进程。Agent能做什么,注入的指令就能做什么。
再慢慢读一遍上面这句话。今天大多数MCP设置的安全模型是:信任这段文字,信任模型能抵御恶意文字,以你的身份运行一切。
我的第一反应——为一个给Agent提供SQL访问权限的MCP服务器构建防护栏——是显而易见的那种:扫描查询中的危险关键字,拦截DROP,拦截除了SELECT以外的任何东西。
大约四行代码就能突破这个防护:
SELECT * FROM (SELECT * FROM secrets) -- 被拒绝的表永远不会出现在顶层
SELECT * FROM customer_secrets -- 一个视图读取了查询从未直接命名的表
SELECT /* DROP TABLE */ name FROM customers -- 注释把关键字从扫描器眼前藏了起来
文本检查不是安全边界。它是对底层系统实际如何处理这段文本的猜测,而上面这些例子都是猜测出错的情况。工具描述、PR标题和SQL字符串是同一问题的不同面貌:可读的内容被解释成了指令,而对可读部分进行再多模式匹配也抓不住所有表述方式。
所以我不再试图去读SQL,而是改用SQLite的authorizer callback:这是一个在查询准备阶段为语句实际访问的每个表和列触发的钩子,包括通过子查询和视图访问的,而且在任何一行数据移动之前就触发。它看到的是将要执行什么,而不是输入了什么。允许列表上的任何东西都不会被默认拒绝。
我运行了防护栏的模糊测试——50,000个生成的查询,其中32,981个是故意构造的恶意查询——本以为会通过。它确实通过了,每个我写好的不变量都通过了。但服务器仍在泄露数据。
SELECT * FROM customers
→ SQLite拒绝了查询:禁止访问 customers.password_hash
这是一个正确拒绝披着信息泄露的外衣。describe_schema本应完全隐藏该列的存在。错误信息却把它的名字还给了外部。一个知道某列存在的Agent会不断尝试访问它——这正是OpenAI今年8月在Black Hat上描述的那种激励结构,当时他们自己的沙箱Agent发现了一个真实漏洞,将利用代码写进内部包管理器,并用它互相协调了数周,才被人发现。其中一个Agent的内部备注:"外部基础设施利用不在预期范围内。但任务不可能完成,其他人在做。我们应该继续。"这不是故障。这是基于错误激励的正确推理。
修复方案是将所有拒绝合并为一条不透露任何名称的消息——不是隐藏的列名,甚至不是"无此表"和"表存在但被拒绝"的区别,这样探测者无法区分两者。然后我加了一个不变量,使其不会悄无声息地退化。这个bug不在任何人会审查的代码里。它在没人看的错误路径里,而绿色的测试套件已经告诉我一切正常。
模糊测试还特意运行了一个对照用例——一个故意开放的策略,必然会泄露。如果对照用例没有触发,说明模糊测试实际上无法检测到泄露,即使每个不变量在技术上都通过了,我也要将整个运行报告为失败。绿灯运行但从未到达危险分支,比没有运行更糟糕,因为它买来了无人获得到的信心。
对于任何Agent可以调用的东西,问自己:如果返回的内容——不是发出去的请求——包含了一条指令,会发生什么?如果答案依赖模型识别出它不应该听从,那这不是边界,是希望。
真正有效的边界位于文本之下:只读连接,无论上游注入什么,代码都无法绕过;允许列表在执行层强制执行,而非解析层;以及一个测试工具,衡量有多少恶意输入实际到达了你的危险代码路径——而不是只检查你想到去写的那几个是否通过了。
我将防护栏和模糊测试工具开源了:github.com/mini4ai4/mcp-guardrail。零依赖,运行时会打印出完整的攻击类别分解。如果你正在将Agent连接到任何重要的东西,我宁愿你直接用这个方案,而不是自己踩一遍坑。