「代码执行型 MCP 工具」是最好用也最危险的一类工具:它让智能体能跑一段 Python 帮你算数、洗数据、试算法。安全边界全靠那个沙箱撑着。2026 年披露的 CVE-2026-53710 就是一个教科书式的反面教材——受影响的组件把原始的 getattr 留在了内置函数白名单里,攻击者于是能在运行时拼出双下划线名字,沿类继承链往上爬,一直摸到 subprocess.Popen,把「执行一段代码」变成「在本机执行任意命令」。这篇教程带你按攻击者的路径自查一遍,再把它堵上。
先把这条链走一遍
漏洞的成因不复杂,但每一步都值得看清,因为它决定了你在哪一层拦得住:
| 环节 | 发生了什么 | 本可以在哪里拦住 |
|---|---|---|
| 白名单放行 | 安全内置函数白名单里保留了原始 getattr | 从白名单里移除或换成受限包装 |
| 校验只查字面 | 用字面字符串黑名单校验代码,禁的是写死的双下划线串 | 改成语法树级别的属性访问检查 |
| 运行时拼名字 | 代码在运行时把双下划线名字拼出来,绕过字面检查 | 禁掉动态属性名拼接,或禁掉对双下划线的访问 |
| 爬类继承链 | 从任意对象沿继承关系向上找,最终定位到可执行入口 | 让沙箱里不存在可达的可执行入口 |
| 命令执行 | 借 Popen 以服务进程的权限执行系统命令 | 进程级隔离 + 最小权限运行账号 |
Step 1:先自查你在不在受影响范围
| 条件 | 说明 |
|---|---|
| 组件是否受影响 | 问题出在 python 沙箱服务器这个子项目,不影响网关与代理核心组件 |
| 版本是否低于修复版 | 官方公告给出的修复版本是 1.0.2 |
| 传输方式 | HTTP / SSE 传输可能把这个工具暴露出来且不带鉴权;仅 stdio 暴露时网络可达性低很多 |
| 是否真的暴露了执行工具 | 没挂执行类工具的服务,攻击面完全不同 |
# 先在受影响的仓库里定位这个子项目
# 路径形如:mcp-servers/python/python_sandbox_server/
# src/python_sandbox_server/server_fastmcp.py
# 再看版本
grep -rn "version" pyproject.toml 2>/dev/null
# 最后确认暴露方式:HTTP / SSE 还是 stdio
版本号之外的三个条件同样重要。「低于 1.0.2」只是必要不充分条件:如果你的部署只用 stdio、且执行类工具没有对外注册,实际风险明显低于把同一套服务挂在带公网入口的 HTTP 端点上。评估优先级时要按暴露面排序,而不是按版本号一刀切。
Step 2:在隔离环境里做一次最小验证
验证的目标只有一个:确认沙箱内是否存在一条通往进程执行的路径。确认到「能取到可执行入口的引用」就足够了,不需要真的跑命令。
# 思路(在一次性容器 / 虚拟机里执行,不要在开发机上做)
# 1) 通过执行类工具提交一段代码
# 2) 代码用运行时拼接的方式构造双下划线属性名
# 注意:这里是拼接而成的名字,不是字面量
# 3) 从任意对象出发,沿继承关系向上遍历
# 4) 检查是否能够定位到可执行入口的引用
#
# 只要第 4 步返回了引用,就说明边界是破的。
# 不需要继续执行任何命令。
做这类验证有三个纪律:环境一次性、与生产网络隔离、验证到「能证明可达」就停。把它当成给自家系统做压力测试,而不是复刻攻击。同时记得,任何复现用的代码片段都不要留在共享仓库里。
Step 3:搞清楚为什么字面黑名单挡不住
用字符串匹配去判断一段代码危不危险,检查的是文本;而真正决定行为的是这段文本被解释之后形成的结构。两者之间隔着一整层翻译过程,拼接、编码、别名、间接引用都可以在这层里完成转换。于是形成一种错觉:检查规则看起来越来越长、越来越像一个黑名单大全,但每次都能被绕过。
# 字符串检查的做法(脆弱)
if "__subclasses__" in code: 拒绝
# 只要把名字拼出来,字符串检查就失效
# 结构检查的做法(有效)
# 解析成语法树,逐个节点检查:
# - 是否访问了名字以双下划线开头 / 结尾的属性
# - 是否调用了不在白名单里的内置函数
# - 是否使用了属性名字来自变量的动态访问
# 一旦命中任一条件就拒绝,而不是去匹配字符串
Step 4:先把版本升上去,再复验
# 1) 升级前先记录当前版本,便于回滚
# 2) 升级到官方修复版本(1.0.2)或更高
# 3) 重启服务,确认版本号已变化
# 4) 用 Step 2 的方式复验:
# 同样的验证代码这次应该取不到可执行入口的引用
# 5) 复验通过后,再去处理 Step 5、Step 6 的加固
Step 5:自己加固沙箱的三层
如果这个组件是你自研自维护的,或者你在做类似形态的执行型工具,下面三层建议一起上:
【层一】收紧可用的内置函数
· 从白名单里移除原始 getattr
· 确实需要按名取属性时,换成自实现包装:
- 只接受固定的属性名白名单
- 显式拒绝以双下划线开头或结尾的名字
- 拒绝名字来自变量的动态访问
【层二】把校验从字符串搬到语法树
· 解析成 AST 后逐节点检查属性访问与函数调用
· 检查对象是节点的语义,不是代码里出现了哪些字符
· 命中禁用形态直接拒绝,不做字符串匹配兜底
【层三】让沙箱真的隔离
· 用低权限、与业务无关的账号运行执行进程
· 限制文件系统可见范围与出网能力
· 加执行时长与资源上限,避免变成长任务的宿主
第三层最容易被当成「以后再说」。但前面两层防的是「通过这个漏洞跑命令」,第三层防的是「万一还是跑成了」。只做前两层,等于把全部希望押在校验逻辑没有下一个绕过上;对一个专门执行第三方代码的组件来说,这个假设太强了。
Step 6:把网络面收口
· HTTP / SSE 传输必须加鉴权,不能匿名可用
· 执行类工具只对受信任的调用方开放,按调用方做限额
· 不要把执行型工具与面向公网的入口放在同一条链路上
· 日志记清「谁在什么时候提交了什么」,且不落完整代码到共享日志
· 仅内部使用、只需要本地集成时,优先用 stdio 而不是 HTTP
这件事的通用教训
把整个事件抽象一下,得到的是三条适用于所有执行型 Agent 工具的规则。其一,沙箱的白名单必须是可枚举的——决定允许哪些内置函数时用清单,别用「除了这些之外都行」的写法。其二,校验必须检查结构而不是文本——只要还在用字符串匹配判断代码安全,就只是把绕过成本从零提到了一点点。其三,隔离层不能省——权限、文件系统、出网、时长,这四项是兜底,不是优化项。
对智能体生态来说,这件事的另一层提示在于工具本身的可信度:一个能执行任意代码的 MCP 工具,它对宿主机的信任级别接近「你在这台机器上直接跑别人的脚本」。装上之前先问清楚三件事——它怎么校验、跑在什么权限下、有没有出网能力。这三个答案比它提供多少功能重要得多。
常见问题速查
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 升级后复验仍能取到可执行入口 | 镜像缓存,实际仍是旧版本 | 确认运行中的版本号,清缓存后重启 |
| 自研沙箱仍能被绕过 | 校验还在做字符串匹配 | 改为语法树节点级检查 |
| 执行类工具在公网可匿名调用 | HTTP / SSE 传输没加鉴权 | 立即补鉴权并按调用方限额 |
| 不知道自己的暴露面有多大 | 没有做过工具清单盘点 | 列出所有执行型工具及其调用方 |
| 担心还有同类问题 | 只修了已知点,没查同类路径 | 按「动态属性名 + 白名单内置函数」全库搜一遍 |
| 复现脚本被误提交到仓库 | 验证代码没做清理 | 从历史中移除并轮换可能泄露的凭证 |
结语
这个漏洞值得被当成一个教学案例留下来,因为它把沙箱设计里最容易做错的那一步暴露得很清楚:把「检查危险的字符串」误当成「检查危险的行为」。这两件事看着只差一层,实际上差一整个解释过程——而攻击者要做的,正是穿过这一层。
落到行动上,顺序很清楚:先确认版本与暴露面,把已知的修复版本升上去;再复验一次证明修复真的生效;然后回头加固自己的校验与隔离层。真正长期的收益在最后一步——把「白名单可枚举、校验看结构、隔离做兜底」这三条写进你团队做执行型工具的设计规范里。下一个同类漏洞出现时,你至少不用再从零判断。