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 |
| CVSS | 9.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,修复动作很小:
- 升级到 2.1.27 或更高版本(补丁包含三个修复 PR);
- 所有 SSE 部署设置
SSE_AUTH_TOKEN——即使升级了,这个环境变量也不会自动出现,需要显式配置。
在此之前运行的版本,按"已泄漏"处理:轮换该服务器使用的 GitLab PAT,并检查 CI 变量是否被异常读取。
五、加固清单:给所有自建 MCP 服务器
把这条链抽象掉具体产品,得到的是一份通用清单。每一条都对应这条链上的一环:
1. 传输层:有网就必有认证
stdio 模式可以裸奔(本机进程边界就是信任边界),但 SSE/HTTP 模式必须有 token 校验。最朴素的实现——校验 Authorization 头——也比没有强得多:
// 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. 文件类工具:白名单根目录 + 路径规范化
任何接受路径参数的工具,都要把路径限制在明确的工作根目录内:
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 够不够小。五分钟,可能省下一次完整账号接管。

