Skip to content

@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

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:

  1. The fix version is a moving target. The execute_graphql/allowlist bypasses were reviewed on 2.1.28after 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 populates first_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 npm latest is 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.27 with first_patched_version 2.1.27, and the same API pass confirms CVE-2026-61568 is bounded at < 2.1.30.)
  2. 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 shipped docker-compose.yaml is built for. GET /sse hands out a sessionId with 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=true option 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-enabled users toolset) calls fs.readFileSync(filePath) where filePath comes straight from user input — the Zod schema declares file_path as a bare z.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 → capture sessionIdlist_projects to find a writable project → upload_markdown with file_path=/proc/self/environ → fetch the uploaded file from GitLab → extract GITLAB_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

  1. 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 set SSE_AUTH_TOKEN — the fix is registration/transport gating plus an explicit token; patching without configuring the token keeps you exposed.
  2. 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 /sse or /messages requests is a credential vault with an open door. tools/list hands an attacker a machine-readable capability catalog by design.
  3. 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.
  4. Hunt artifacts: unauthenticated GET /sse and POST /messages?sessionId= traffic on port 3002 (or any reverse-proxied MCP path) from non-administrative source ranges; GitLab-side tells = upload objects named environ/cmdline/dotfiles appearing in projects, and API use of a PAT from the MCP server's host plus other hosts.
  5. 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.1 unless you deliberately expose the service.
  6. 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.

Sources