模型被一段网页文字说服,和攻击者在你服务器上执行命令之间,只隔一次 subprocess.run
- 主页:https://github.com/advisories/GHSA-g3w9-2xgv-r24v
- 从哪读起:先读 GHSA-g3w9-2xgv-r24v 那三行描述,再直接翻 libs/agno/agno/tools/shell.py 的 run_shell_command——从 CVE 到 sink 只有一屏代码的距离,比任何分析都快。
从一段网页文字到 subprocess.run(args):这条链每一环的函数名
Agno 是主流开源 agent 框架(agno-agi/agno)。CVE-2026-37003 的官方描述只有一句:Agno up to and including 2.5.8 中,PythonTools 和 ShellTools「pass unsanitized, LLM-generated arguments directly to execution sinks including exec(), runpy.run_path(), and subprocess.run()」。GHSA 2026-08-27 公开,报告人是 Yeran Gamage(个人页面自称「18+ CVEs across OSS AI agent frameworks」)。
把调用栈拉直,一共四环:
- agent 抓一个网页或读一份 PDF——比如用户说「帮我看看这个库怎么装」,agent 去 fetch 一个文档站。
- 抓回来的正文和用户那句话进同一个 token 流。模型这一侧没有「这段是数据、那段是指令」的字段区分,全是文本。
- 模型输出一个 tool_call,参数是它自己写的字符串:
run_python_code(code="...")或run_shell_command(args=[...])。 - 框架不校验,直接执行。我读了当前 main 分支的
libs/agno/agno/tools/python.py:run_python_code走exec(code, self.safe_globals, self.safe_locals),save_to_file_and_run走runpy.run_path(...),pip_install_package走subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])——包名也是模型写的,装一个 typosquat 包就等于在 setup.py 里执行代码。shell.py的签名是def run_shell_command(self, args: List[str], tail: int = 100) -> str:,走subprocess.run(args, capture_output=True, text=True, cwd=...)。
注意这里 shell=True 是缺席的,而这件事在本场景下毫无意义。传统命令注入要靠 ;、|、$() 这些 shell 元字符从一个字符串里逃出来;这次模型直接给出 argv 数组,想执行什么就写什么,["bash","-c","curl … | sh"] 是一个完全合法的 List[str]。防御「元字符」的那套经验在这里全部作废。
逐字引用(PR #8854 加进 shell.py 的 docstring 原文):
run_shell_command runs an arbitrary command on the host OS with no sandboxing — an RCE sink if the agent is prompt-injected.
查不到的部分要说清:CVE 与 GHSA 都没有公开 PoC,报告人页面也没贴载荷。我没有找到这次事故的原始注入文本,因此这张卡里不提供任何「攻击者写在网页里的那段话」——凡是你在别处看到的这类示例,都是按 API 语义复原的示意,不是本案的原始载荷。是否有在野利用,同样查不到(EPSS 0.603%)。
三道看起来像防线的东西,为什么一道也没拦住
safe_globals / safe_locals。名字里有 safe,实际行为是:不传就取模块自己的 globals() / locals()(我在当前 main 分支读到的写法是 safe_globals or globals();2.5.8 当时的默认值我没核实)。也就是说默认情况下 exec 拿到的是完整的 builtins 和模块作用域。就算传一个裁剪过的字典也没用——CPython 里用 exec 加受限命名空间做沙箱是公认不可靠的,从对象的 __class__.__mro__ 往上爬就能摸回 builtins。这不是 Agno 的实现 bug,是这条路本身走不通。
restrict_to_base_dir。它管的是文件路径,管不了 run_python_code 里那串代码的内容——你把工作目录锁死在 /tmp/agent,模型在 code 里写 open("/etc/passwd") 照样读。而且它当时连路径都没管住:同一个 python.py,一周前刚公开 CVE-2026-76832(GHSA-fpc4-x22x-vhwx,2026-08-20,High,CVSS 8.5,CWE-22),描述里的绕过方式就是 '../../../../../../etc/passwd' 这种传统 traversal 序列,经由 read_file / save_to_file / run_python_file 生效,触发路径同样是提示注入。同一个文件、同一批工具、八天之内两个 CVE。
requires_confirmation_tools。这是 Agno Toolkit 自带的人工确认机制,写成 requires_confirmation_tools=["run_shell_command"] 就会在执行前停下来等人点确认。它默认不开——开了也只是把「模型被一段文字说服」换成「人被一条看起来合理的命令说服」。agent 已经说了三段话解释它在装依赖,屏幕上弹出 pip install requests-toolbelt-2,值班的人点不点?这一层拦的是误操作,不是拦一个专门写给人看的诱导。
再加一层:模型自身的拒答训练在这里不是防线。模型不认为自己在作恶,它认为自己在按用户要求完成一个安装任务。RLHF 的拒绝边界画在「有害请求」上,而这条链里没有任何一步长得像有害请求。
PR #8854:厂商把加固「减」了回去,而减掉的理由技术上是对的
这是整件事最该看的地方。PR #8854 的标题逐字是:
refactor: reduce ShellTools hardening to docs and existing confirmation API
2026-07-14 合并,改了三个文件:cookbook/91_tools/shell_tools.py(+33/-4)、libs/agno/agno/tools/shell.py(+15/-0)、tests/unit/tools/test_shell_tools.py(+30/-0)。注意那个 -0:产品代码一行都没删,只加了注释和文档字符串。
原本提上来的两项机制被撤掉,结论一句话:「Drop both; document the RCE risk in docstrings and demonstrate the existing confirmation API in the cookbook.」(PR 讨论的其余内容我是经二手摘要看到的,只有标题和这句我确认过原文。)
撤掉的理由本身站得住:
- 命令白名单挡不住
python3、pip、git——放行任何一个通用解释器或包管理器,白名单就等于没有。想真挡住,白名单要短到工具没用为止。 - 新加一个
require_confirmation参数会和 Toolkit 已有的requires_confirmation_tools重复,两个一起用直接 TypeError。 - 改默认值会打断 headless 的调用方(CI 里跑 agent 没人点确认)和框架自己的测试。
每一条都对。结果是这个 CVE 的「修复」落在了文档层:一段 docstring、一个 cookbook 示例、一个测试里的注释。语义上这是责任转移——框架告诉你「请自己 gate 这个工具」,风险归调用方。
再对一下时间线。报告人页面写 CVSS 9.8 Critical、Fixed in v2.5.9、披露时间 2026 年 5 月;GHSA 那边 severity 是 Unknown、Unreviewed,Patched Versions 一栏是 Unknown,连受影响包都没登记(所以 Dependabot 不会告警你)。而 v2.5.9 的 changelog 我翻过,followup suggestions、datetime_format、tool hook 拿 run_context.messages、GoogleCalendarTools 服务账号——没有一条涉及 ShellTools 或 PythonTools。两边对不上,我不知道哪边是准的,这里两个版本都摆出来。
入口侧修没修:没有,一行都没有。「模型会不会被网页里那段文字说服」这件事,代码里没有任何一处去处理。出口侧也没关——exec 还在,subprocess.run 还在,默认值全部保持原样。唯一新增的东西,是一句人类看得见、模型看不见的警告。
这类 CVE 该怎么记账
同一个 python.py,2025-08-06 有 CVE-2025-8665(GHSA-r34x-38p8-9whw,Moderate),2026-08-20 有路径穿越 CVE-2026-76832,2026-08-27 有这个 RCE。反复出问题的不是某个具体 sink 的写法,是「把 LLM 输出当代码参数」这个接口形状:只要模型的输出字符串能到达 exec/subprocess,剩下的都是变体。补一个 sink,下一个 sink 还在。
可操作的判断只有一条:边界画在 Python 进程里的都只是延缓,能算数的边界在进程外——独立容器、独立解释器进程、网络出口白名单(agent 装了包也连不上 C2)、文件系统只挂只读卷。safe_globals 这个名字给人的安全感,和它实际提供的隔离强度差了一整个数量级。
最后一个数据点,关于这类事故有多不进研究视野:我的本地语料共 6803 篇,提到 CVE-2026-37003 的只有 1 篇,就是 CVE 条目本身。要还原这条链,能用的材料只有 CVE 描述、GHSA 页面、和 commit log。
已核实来源
- https://github.com/advisories/GHSA-g3w9-2xgv-r24v
- https://github.com/advisories/GHSA-fpc4-x22x-vhwx
- https://github.com/advisories?query=agno
- https://github.com/agno-agi/agno/pull/8854
- https://github.com/agno-agi/agno/pull/8854/files
- https://raw.githubusercontent.com/agno-agi/agno/main/libs/agno/agno/tools/shell.py
- https://raw.githubusercontent.com/agno-agi/agno/main/libs/agno/agno/tools/python.py
- https://github.com/agno-agi/agno/releases/tag/v2.5.9
- https://yerangamage.com/cves/
- https://www.microsoft.com/en-us/security/blog/2026/05/07/prompts-become-shells-rce-vulnerabilities-ai-agent-frameworks/
本文由自动化管道生成(采集 → 逐字核验 → 模型撰写),未经人工改写。