@zereight/mcp-gitlab CVE-2026-61560: unauthenticated SSE transport plus arbitrary file read exfiltrates the GitLab PAT (CVSS 9.8, Sep 16, 2026)
Summary
The GitHub security advisory for @zereight/mcp-gitlab — the most popular community GitLab MCP server (npm, ~2,000 GitHub stars, ~82,000 npm downloads/week at the time of the advisory) — publishes CVE-2026-61560 (CVSS 9.8, published September 16, 2026): in SSE transport mode (SSE=true), which the project's own docker-compose.yaml presents as the intended Docker deployment, the /sse and /messages endpoints carry zero authentication, and the default-enabled upload_markdown tool reads any file on the server's filesystem via an unsanitized file_path parameter and uploads it to a GitLab project. Chained, any network-reachable attacker connects without credentials, reads /proc/self/environ (which contains GITLAB_PERSONAL_ACCESS_TOKEN in plaintext), retrieves the uploaded file from GitLab, and owns whatever the PAT can touch — repositories, issues, CI variables, the whole account. The container runs as root (the Dockerfile has no USER directive) and the compose file maps 3002:3002, binding 0.0.0.0. REMOTE_AUTHORIZATION is explicitly incompatible with SSE mode in this codebase, so there was no per-request auth to enable. Patched in 2.1.27 (auth token gating + path restrictions landed via PRs #482/#554/#622, merge 73d198c); maintainers ask deployers of v2.1.27+ to run with SSE_AUTH_TOKEN set. This is the internet-exposed-unauthenticated-MCP-server pattern instantiated in a single mass-adopted package: the MCP server is a privileged GitLab proxy with an anonymous front door.
Tags
- tools
- mcp-gitlab
- zereight
- CVE-2026-61560
- GHSA-cv3r-c5h8-f4g5
- MCP
- Model Context Protocol
- SSE transport
- unauthenticated access
- arbitrary file read
- path traversal
- PAT theft
- GitLab
- credential theft
- account takeover
- AI agent tooling
- npm
- Docker defaults
- CVSS 9.8
September 16 (evening) update: CVE-2026-61560 is one of SIX advisories on this package — the "2.1.27 fixes it" boundary does not hold
The repo advisory list (github.com/zereight/gitlab-mcp/security/advisories, verified via the GitHub API Sep 16, 2026) carries six published advisories for the same package — five siblings beyond GHSA-cv3r-c5h8-f4g5:
| Advisory | CVE | Severity | What it defeats |
|---|---|---|---|
| GHSA-2h44-8472-frjj | CVE-2026-61559 | critical (report CVSS v3.1 8.5, S:C) | SSRF via X-GitLab-API-URL header when ENABLE_DYNAMIC_API_URL=true: the header is only syntax-checked (new URL()) and the victim's Private-Token is attached to every outbound fetch to the attacker's host — straight PAT theft. "Patched versions: None at time of report." |
| GHSA-vmp7-252j-cwp7 | CVE-2026-61568 | critical | DNS rebinding to the local Streamable HTTP transport (CWE-350): no Host/Origin allowlist; express.json() is installed globally before any MCP route guard, so a malicious web page routes browser requests to a victim's local MCP listener. Confirmed on 2.1.18. |
| GHSA-5648-rgj9-v224 | — | high | Five defects defeating the package's own safety controls (reviewed on 2.1.28): execute_graphql defeats BOTH read-only mode and GITLAB_ALLOWED_PROJECT_IDS; Streamable HTTP /mcp unauthenticated under cookie-jar/device-flow credentials; SSE unauthenticated by default with no Origin/Host validation; unauthenticated session/transport-exhaustion DoS (token check is syntactic only); verbatim CI job-trace return (prompt-injection vector under the MCP threat model). |
| GHSA-7c3w-fxgh-frc7 | — | high | job_id path traversal: project_id is URL-encoded but job_id is interpolated raw, so job_id=../../../user escapes /projects/{id}/jobs/... to an arbitrary /api/v4/ endpoint under the operator token (confirmed on 2.1.18: returned the admin user object). |
| GHSA-4rm9-rfp2-j39q | — | high | download_release_asset path escape via unrestricted direct_asset_path, normalizing out of the release-download path and bypassing GITLAB_ALLOWED_PROJECT_IDS as a scope control (tested on 2.1.30). |
Two consequences change the defender guidance above:
- The fix version is a moving target. The
execute_graphql/allowlist bypasses were reviewed on 2.1.28 — after the 2.1.27 CVE-2026-61560 patch — and the release-asset escape was tested on 2.1.30. None of the three CVE-less siblings populatesfirst_patched_version; they express the fix only as a vulnerable range — GHSA-5648-rgj9-v224< 2.1.30, GHSA-7c3w-fxgh-frc7< 2.1.32, GHSA-4rm9-rfp2-j39q< 2.1.41(verified via the repo-advisories API, Sep 16 late UTC) — so the effective floor implied by the full advisory set is ≥ 2.1.41, while npmlatestis already 2.1.63 (published Sep 16, 2026, 21:07 UTC) with a fast release cadence (2.1.58–2.1.63 in two weeks). Practical rule: run the newest release, not a pinned "patched" version — a deployment pinned at the 2.1.27 "fix" for CVE-2026-61560 is still inside three sibling vulnerable ranges — and re-verify before every deploy. (Earlier in the day the GHSA page for CVE-2026-61559 still read "Patched versions: None at time of report"; the API now bounds it at< 2.1.27withfirst_patched_version 2.1.27, and the same API pass confirms CVE-2026-61568 is bounded at< 2.1.30.) - This is a systemic-boundary story, not one bug. Ten weeks, six advisories, one package — unauthenticated transport, SSRF header trust, missing Host/Origin validation, allowlist bypasses, and two API-path traversals all defeat the same safety model (read-only mode, project allow-list, transport auth). Under the MCP threat model the advisory itself invokes (tool arguments shaped by prompt injection or a malicious client), any one of these is a full-credential event because the server holds a PAT. Treat a pinned older deployment as vulnerable regardless of version number, and keep the PAT-scope and rotation guidance below.
Vulnerability mechanics
- Surface: SSE transport (
SSE=true) — the mode the shippeddocker-compose.yamlis built for.GET /ssehands out asessionIdwith no authentication middleware;POST /messages?sessionId=...then executes any of ~100+ registered tools using the server's configured PAT. - Auth dead-end: the project's
REMOTE_AUTHORIZATION=trueoption is explicitly incompatible with SSE mode in the code (the server rejects the combination), so the documented Docker deployment had no way to authenticate callers. - File-read primitive:
upload_markdown(in the default-enableduserstoolset) callsfs.readFileSync(filePath)wherefilePathcomes straight from user input — the Zod schema declaresfile_pathas a barez.string()with no allowlist, sandboxing, or path restrictions. - Exfiltration channel: the file is uploaded through the legitimate GitLab API (
POST /projects/:id/uploads) and returned as a fetchable/uploads/<hash>/<name>URL — the victim's own GitLab becomes the drop site. - Full PoC path (published in the advisory): connect to
/sse→ capturesessionId→list_projectsto find a writable project →upload_markdownwithfile_path=/proc/self/environ→ fetch the uploaded file from GitLab → extractGITLAB_PERSONAL_ACCESS_TOKEN=glpat-...→ use the PAT as the account. Container runs as root, so/proc/self/cmdline, SSH keys, and mounted secrets are also readable.
Defender heuristics
- Version-audit: upgrade to ≥ 2.1.41 (the highest vulnerable-range floor across all six advisories — 2.1.27 covers only CVE-2026-61560/-61559; the siblings extend the floor to
< 2.1.30,< 2.1.32,< 2.1.41; newest release is safest) and setSSE_AUTH_TOKEN— the fix is registration/transport gating plus an explicit token; patching without configuring the token keeps you exposed. - Audit every exposed MCP endpoint you own, this package or any other: an MCP server holding a PAT, API key, database role, or cloud credential and answering anonymous
/sseor/messagesrequests is a credential vault with an open door.tools/listhands an attacker a machine-readable capability catalog by design. - Rotate now if you ran a vulnerable default deployment: the PAT in the container environment must be assumed burned — revoke it at GitLab (admin-level revoke, not just user delete), and audit the account's recent API activity and project uploads for unexpected
/uploads/objects. - Hunt artifacts: unauthenticated
GET /sseandPOST /messages?sessionId=traffic on port 3002 (or any reverse-proxied MCP path) from non-administrative source ranges; GitLab-side tells = upload objects namedenviron/cmdline/dotfiles appearing in projects, and API use of a PAT from the MCP server's host plus other hosts. - Design rule for MCP deployments: put the server behind authentication at the transport layer you actually ship; never rely on "the client won't call that tool"; restrict toolsets to what the integration needs (this read lived in a default-on toolset); run containers as non-root; keep PATs out of world-readable process environments where a file-read primitive can reach them; bind Docker ports to
127.0.0.1unless you deliberately expose the service. - Ecosystem read: one widely used package shipped an unauthenticated arbitrary-file-read-to-credential-exfiltration chain as its default documented deployment. Treat MCP servers as Tier-1 network assets in inventory and exposure scanning, exactly like the Bifrost management API finding from the same fortnight.
Related pages
- Internet-exposed unauthenticated MCP servers (pattern)
- Bifrost CVE-2026-90898: unauthenticated MCP stdio client-registration RCE
- MCP stdio command-execution boundary (pattern)
- Wiz "Off Guard" LiteLLM MCP bypass → cloud compromise
Sources
- GitHub Advisory: GHSA-cv3r-c5h8-f4g5 — @zereight/mcp-gitlab: Unauthenticated arbitrary file read via
upload_markdownenables PAT exfiltration and full account takeover (CVSS 9.8, patched 2.1.27, published 2026-09-16) - NVD record: CVE-2026-61560
- Fixes: PR #482 · PR #554 · PR #622 · v2.1.27 release