Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

CVE-2026-61560:一个 MCP 默认配置如何变成 GitLab 账号接管链

2026 年 9 月 15 日披露的 CVE-2026-61560(CVSS 9.8)瞄准了使用量最大的 GitLab MCP 服务器之一 @zereight/mcp-gitlab。没有零日漏洞、没有内存破坏——整条利用链的每一环都是开发者熟悉的"默认配置",但三者叠加后,任何能访问端口的人都可以拿走服务器的 GitLab 完整账号权限。本文逐环拆解这条链,并把它还原成一份自建 MCP 服务器的加固清单。

一、漏洞档案

先看基本盘:

项目内容
CVE 编号CVE-2026-61560
CVSS9.8(Critical)
披露日期2026-09-15
受影响组件@zereight/mcp-gitlab(npm 包)
受影响版本全部 < 2.1.27
修复版本2.1.27
安全公告GHSA-cv3r-c5h8-f4g5
攻击前提端口网络可达即可,无需用户交互
核心弱点CWE-22(路径遍历)+ 传输层无认证 + 凭据驻留进程环境

@zereight/mcp-gitlab 是一个让 AI Agent 驱动 GitLab 的 Model Context Protocol 服务器:列出项目、读 Merge Request、上传文件、创建 issue。它的定位决定了它必须持有高权限凭据——服务器的 GitLab Personal Access Token 就配置在它自己的环境变量里。这一点先记住,利用链的第三环会用到。

二、利用链:四步账号接管

整条链的精妙之处在于:每一步用的都是服务器的"正常功能"。没有内存破坏,没有奇异协议,只有三个被默认放行的坏实践串在一起。

攻击者

  ├─① 连接 SSE 端点(SSE=true 默认开启,零认证)
  │      └─ 拿到全部 MCP 工具的调用权

  ├─② 调用 upload_markdown,file_path=/proc/self/environ
  │      └─ 未过滤的路径参数 → 读取服务器本地任意文件
  │      └─ 内容被"正常"上传到某个 GitLab 项目

  ├─③ 从上传内容中提取 GITLAB_PERSONAL_ACCESS_TOKEN
  │      └─ 服务器的凭据就在它自己的环境变量里

  └─④ 用该 token 以受害者身份操作 GitLab
         └─ 读写全部可访问仓库 / 批准 MR / 读取 CI 变量

第 1 环:SSE 传输默认无认证

在 SSE(Server-Sent Events)传输模式下——SSE=true,也是 Docker 部署的默认值——服务器把全部 MCP 工具暴露在网络端口上,没有任何认证检查。没有 token、没有登录、没有握手。只要端口网络可达,攻击者就等于拿到了 Agent 的身份。

这是 MCP 生态的一个系统性问题:stdio 传输天生只有本机进程能用,但一旦切到 SSE/HTTP 传输让远程 Agent 连接,认证就从"协议默认"变成了"开发者自觉"。此前 Go MCP SDK 的 CVE-2026-34742(localhost 默认裸奔)与本站此前分析过的 Postgres MCP Pro、AWS MCP 系列漏洞,都是同一命题的不同变体。

第 2 环:upload_markdown 的路径遍历

upload_markdown 工具接受一个 file_path 参数,本意是读取本地 Markdown 文件再上传。但参数未经净化(CWE-22),攻击者把路径指向:

file_path=/proc/self/environ

/proc/self/environ 是 Linux 上当前进程的环境变量块,以 \0 分隔。服务器读取该文件后,按工具的设计把内容上传到某个 GitLab 项目——于是文件内容就以"一次合法上传"的形式离开了服务器本地。

这一步在防御方眼里格外值得玩味:渗出通道是一个正常功能。日志里出现的不过是一次平平无奇的"上传 Markdown 文件到 GitLab",与开发者日常操作无异。工具做了它被造出来要做的事,只是从没问过"谁在调用"以及"这个文件该不该它读"。

第 3 环:凭据就在 /proc/self/environ 里

服务器自己的 GITLAB_PERSONAL_ACCESS_TOKEN 就配置在它的进程环境里——也就是说,它明文躺在 /proc/self/environ 中。攻击者从第 2 环上传的内容里直接提取 token。

第 4 环:接管完成

有了 Personal Access Token,攻击者就是受害者:读写所有可访问仓库、批准 Merge Request、读取 CI/CD 变量(那里往往还有下游部署凭据)。账号接管完成,全程无用户交互。

三、为什么是 9.8:三环缺一不可

单独看每一环,都是安全审计里的常见发现:

环节单独修复后的效果
SSE 无认证攻击者无法触达任何工具,链断在第 1 步
路径遍历攻击者读不到 /proc/self/environ,链断在第 2 步
凭据驻留环境变量即使读了 environ 也拿不到有效 token,链断在第 3 步

评级 9.8 的原因不是某一环特别惊艳,而是三环全部出现在同一份默认配置里:Docker 部署默认开 SSE,SSE 默认不带认证,文件工具默认不净化路径,而凭据默认就放在环境变量里。这正是自建 MCP 服务器生态的缩影——每一层的安全都指望"上层会兜住",结果没有任何一层真的兜了。

四、修复与止血

如果你在跑 @zereight/mcp-gitlab,修复动作很小:

  1. 升级到 2.1.27 或更高版本(补丁包含三个修复 PR);
  2. 所有 SSE 部署设置 SSE_AUTH_TOKEN——即使升级了,这个环境变量也不会自动出现,需要显式配置。

在此之前运行的版本,按"已泄漏"处理:轮换该服务器使用的 GitLab PAT,并检查 CI 变量是否被异常读取。

五、加固清单:给所有自建 MCP 服务器

把这条链抽象掉具体产品,得到的是一份通用清单。每一条都对应这条链上的一环:

1. 传输层:有网就必有认证

stdio 模式可以裸奔(本机进程边界就是信任边界),但 SSE/HTTP 模式必须有 token 校验。最朴素的实现——校验 Authorization 头——也比没有强得多:

javascript
// SSE 传输下的最小认证中间件(Node/Express 风格)
app.use("/sse", (req, res, next) => {
  const auth = req.headers["authorization"];
  if (auth !== `Bearer ${process.env.SSE_AUTH_TOKEN}`) {
    return res.status(401).json({ error: "unauthorized" });
  }
  next();
});

同时做网络层收口:MCP 服务器绑 127.0.0.1 或内网段,不要 0.0.0.0 对公网。如果必须远程访问,前置一层网关做认证与审计(本站此前分析过的 MCP 治理面文章有完整方案)。

2. 文件类工具:白名单根目录 + 路径规范化

任何接受路径参数的工具,都要把路径限制在明确的工作根目录内:

javascript
const path = require("path");

const ALLOWED_ROOT = path.resolve("/data/mcp-workspace");

function safeResolve(userPath) {
  // 先规范化,再校验前缀,双防绕过
  const resolved = path.resolve(ALLOWED_ROOT, userPath);
  if (!resolved.startsWith(ALLOWED_ROOT + path.sep)) {
    throw new Error("path traversal rejected: " + userPath);
  }
  return resolved;
}

// 使用:file_path 参数先过 safeResolve
// safeResolve("/proc/self/environ")  → 抛出异常
// safeResolve("../../etc/passwd")    → 抛出异常
// safeResolve("notes/todo.md")       → /data/mcp-workspace/notes/todo.md

要点有三:用 path.resolve 消解 .. 与符号链接的歧义;校验规范化后的前缀而不是字符串匹配原始输入;拒绝绝对路径直接透传。

3. 凭据管理:别把高权限 token 放进程环境

/proc/self/environ 一次即全泄。至少做到:

  • 最小权限:MCP 服务器用的 GitLab PAT 只授予它实际需要的 scope(比如 read_api + 特定项目的 write_repository),不要给 api 全量权限;
  • ** secrets 与进程分离**:使用 secret manager 注入短期凭据,或至少把凭据放在只有按需读取的文件中,而不是长期躺在环境变量里;
  • 可轮换:假设任何进程内凭据都可能被一次任意文件读取带走,轮换成本要低。

4. 渗出通道审计:合法功能也要有边界

这条链最阴的地方是用"上传 Markdown"渗出数据。防御视角下,文件上传类工具应记录:来源会话是否认证、目标文件路径是否在工作区内、上传体积是否异常。配合 OWASP MCP Top 10 里 MCP08 的要求——记录每一次工具调用——这类渗出至少能在事后追溯。很多 MCP 客户端目前连工具调用日志都默认关闭,这一点值得在选型时当作硬指标。

六、它属于哪个"家族"

把近期 MCP 服务器 CVE 摆在一起,能看到清晰的谱系:

  • 认证缺失类:Go MCP SDK CVE-2026-34742(localhost 裸奔)、本文 CVE-2026-61560(SSE 无认证);
  • 输入未净化类:Atlassian MCP CVE-2026-73498(Confluence 上传路径遍历)、本文第 2 环;
  • 输出过载类:Postgres MCP Pro CVE-2026-85620(受限模式绕过)、AWS MCP CVE-2026-87911(命令注入)。

共同点:都不是协议本身的缺陷,而是实现者把"AI Agent 会按预期调用工具"当成了信任模型。MCP 规范在 2026-07-28 版本里补充了更多安全控制,但规范管不住默认配置——真正决定安全水位的是每个自建服务器作者的第一行代码。

七、结语

CVE-2026-61560 没有任何一项"高级"技术:无认证的传输、没写净化函数的路径参数、放在环境变量里的 token——每一个都是十年前就该写进 checklist 的条目。它的 CVSS 9.8 提醒我们:Agent 时代的攻击面 = 每一个新协议层 × 传统的旧错误

自建 MCP 服务器的读者,今天就做三件事:查一遍自己的 SSE 部署有没有认证;文件工具跑一次 ../../etc/passwd 测试;确认进程凭据的 scope 够不够小。五分钟,可能省下一次完整账号接管。

参考资料

上次更新于: