GitLab CVE-2026-90970(CVSS 9.9)暴露 AI Agent 权限边界问题:内部网络隔离不等于安全,Agent 会话内的命令执行可能绕过传统网络鉴权;作者复盘了自己内部 Agent 网关的同类风险。
本月早些时候,GitLab AI Gateway 中出现了一个严重漏洞——CVE-2026-90970,CVSS 评分 9.9,允许拥有 agent 平台访问权限的已登录用户在网关上执行命令。我和大多数人一样阅读了这份公告:点点头,心想"幸好不是我们",然后就翻过去了。
然后我又想了十秒钟我们自己的架构,那股"幸好不是我们"的感觉就消失了。
我们把一个内部 AI agent 接入到了基础设施中——原因很常规:它可以读取日志、查询内部 API、打开 PR、重启不稳定的 worker 之类的。它通过一个小型的内部网关服务与我们的系统通信,把模型的 tool call 转换成实际的操作。这个网关没有暴露在互联网上。没有入站端口,仅限内部 DNS,在 VPN 后面。当时整个安全论证就是:外面访问不到,所以没问题。
我开始排查我们的网关是否存在同一类 bug——未经认证或认证不足的命令执行,且可被 agent 会话触及。我的第一个假设是,风险(如果存在的话)纯粹是网络暴露问题,形状和 GitLab 的问题一样。我检查了两遍网络路径,确认从我们的 VPC 外部没有入口,差点就在那里结束调查了。
第二个假设也差点让我止步:我以为我们的 tool-calling 层有一个 permissions.yaml 文件,列出了每个 agent 角色可以调用哪些工具,这个文件在命令执行时会被实际执行。它看起来就像是 enforcement。有角色名、工具名、allowed: true/false 列。肯定有什么地方在检查它。
但实际上,在真正重要的那个层级,什么都没有在检查。
权限检查活在编排层——也就是决定向模型提供哪些工具的代码。如果某个角色不应该有 restart_service,那个工具 просто不会包含在发给该会话模型的工具列表里。
但网关的实际命令执行端点没有重新检查任何东西。它信任任何到达它的请求都已经在上游被过滤过了。这是一个完全合理的设计——直到有什么东西通过一条跳过了编排层的路径产生了 tool call。结果证明,我们自己的重试逻辑就是这么做的。当一个 tool call 超时时,重试处理器会直接把调用重新提交到网关的执行端点以节省延迟,完全绕过了有权限感知的编排路径,带着 service-account 凭证一起。
换句话说:enforcement 看起来像是服务端授权。实际上是客户端意图,由一个按构造本应服从模型指令而非监管它们的组件来执行。一个被攻陷的、困惑的、或者只是足够有创造力的 agent 会话——甚至不需要恶意,只是出错了——通过重试路径,可以执行网关自身 service account 能做的任何事。这比"一个 AI 帮助重启了一个 worker"的爆炸半径要大得多。
ALLOWED_COMMANDS = {
"restart_service": {"args": {"service_name": r"^[a-z0-9-]+$"}},
"read_logs": {"args": {"service_name": r"^[a-z0-9-]+$", "lines": r"^\d{1,4}$"}},
}
def execute_tool_call(role: str, tool: str, args: dict) -> str:
allowed_tools = ROLE_PERMISSIONS.get(role, set())
if tool not in allowed_tools or tool not in ALLOWED_COMMANDS:
raise PermissionError(f"role={role} is not permitted to call {tool}")
spec = ALLOWED_COMMANDS[tool]
for key, pattern in spec["args"].items():
if key not in args or not re.fullmatch(pattern, str(args[key])):
raise PermissionError(f"invalid or missing argument '{key}' for {tool}")
return run_tool(tool, args)
进入执行的所有路径,包括重试,现在都调用这个函数。没有第二道门。
我们改成了另一种方案:每个 agent 任务获得自己的短生命周期、可支配的沙箱——根本不是一个共享的内部网关。我是 Krova Cloud 的创始人,这正是我们创办这家公司要解决的核心问题——为每次 agent 运行启动一个隔离的 VM,有自己锁定出站-only 的网络,无法访问我们真正的内部系统,在自己的盒子里有 root 权限但别处没有,生命周期以分钟计。如果一个 agent 在这里面跑偏了,损害被限制在一个事后会被丢弃的 VM 里——而不是一个有生产环境 god-mode 访问权限的共享网关。以这种方式运行 agent 会话,每个任务隔离而不是躲在 一个共享特权网关后面,在我们自己的成本对比中,比在 E2B 上等效运行便宜约 93%,比 Modal 便宜约 95%,主要是因为我们不用为按最坏情况并发量 sizing 的空闲共享基础设施付费。
一份权限文件只在上游检查一次,然后在下游被永远信任,这不是 enforcement——这是一份有好心意的建议。
任何绕过正常请求流程的重试、fallback 或"快速路径"都是进入系统的一扇第二道门。要特别审计那些路径,它们是 enforcement 悄悄停止生效的地方。
"没有暴露在互联网上"和"一旦它可以被任何人或任何东西触及,它能做什么"不是同一个问题。一份关于某个攻击面的 CVE 是一个好提示,让你检查你实际拥有的那些攻击面,而不只是上了新闻的那一个。
对于"如果 agent 做了不该做的事"最便宜的真正修复,不是更好的 prompt 或更严格的权限文件——而是确保"agent 做了不该做的事"的爆炸半径是一个可丢弃的沙箱,而不是你真正的基础设施。
如果你现在正把任何类型的 AI agent 对接到真实基础设施上,值得花十分钟问一下你的重试路径上会发生什么,而不只是你的 happy path。
我是 Rohit,Krova Cloud 的创始人——为运行 AI agent 和风险代码提供可丢弃的隔离沙箱,不暴露真实基础设施,成本只是同类沙箱平台的一小部分。如果你想看到更多像这样的深度调试故事,我也会定期在 debugly.dev 上写作。