
One Request to Own Every Repo: How We Hijacked GitLab Through Its MCP Server - Pluto Security
Pluto Security details two critical vulnerabilities in gitlab-mcp (@zereight/mcp-gitlab, 200K+ downloads), the most popular community GitLab MCP server. GHSA-cv3r-c5h8-f4g5 (CVSS 9.8) chains an unauthenticated SSE transport with the upload_markdown tool's unvalidated file_path to read /proc/self/environ and steal the server's GitLab Personal Access Token; GHSA-2h44-8472-frjj (CVSS 9.6) abuses the X-GitLab-API-URL header when ENABLE_DYNAMIC_API_URL=true to make the server attach its PAT to attacker-controlled hosts via SSRF. Both are fixed in v2.1.27 (SSE bearer-auth, non-root Docker, host allowlisting), and the writeup includes full attack commands, root-cause code, and a disclosure timeline.
What it is
Two unauthenticated critical vulnerabilities in @zereight/mcp-gitlab (gitlab-mcp) < 2.1.27, the most popular community GitLab MCP server. Each independently yields the server's GitLab PAT and thus full account takeover. GHSA-cv3r-c5h8-f4g5 (CVSS 9.8, CWE-22) and GHSA-2h44-8472-frjj (CVSS 9.6, CWE-918). No CVEs assigned yet. Fixed in v2.1.27.
How it works
Finding #1 chains two defaults. The SSE transport wires /sse and /messages with no auth middleware, and its per-request auth feature (REMOTE_AUTHORIZATION=true) is explicitly incompatible with SSE mode — there is no supported way to lock it. The upload_markdown tool (in the default-enabled users toolset) takes a bare z.string() file_path, reads it, and uploads it to a GitLab project. Reading /proc/self/environ exfiltrates GITLAB_PERSONAL_ACCESS_TOKEN. The clever part: the stolen data leaves through GitLab itself as a project attachment retrieved over HTTPS, so egress looks like normal traffic to your own GitLab. The shipped Dockerfile has no USER directive (runs as root) and docker-compose maps 3002:3002 (binds 0.0.0.0).
Finding #2: with ENABLE_DYNAMIC_API_URL=true, the X-GitLab-API-URL header sets the outbound API base URL and is validated only by new URL() — a syntax check, not an allowlist. The server then attaches Private-Token: <victim_token> to a request aimed at any attacker host. A confused deputy: one request and the credential walks out.
Preconditions / Affected
- #1: any instance reachable with
SSE=trueand a PAT set — the default Docker posture — plus a writable project ID (enumerable vialist_projects). - #2: any HTTP-transport deployment with
ENABLE_DYNAMIC_API_URL=true. Note this reaches the sink in the documentedREMOTE_AUTHORIZATION=truemode; the earlier startup guard never touched the header-parsing path.
What to do
Upgrade to v2.1.27+ first: npm i @zereight/mcp-gitlab@latest. It adds requireSseAuth bearer middleware, fails closed (refuses to boot when SSE=true on a non-loopback host without SSE_AUTH_TOKEN, unless SSE_DANGEROUSLY_ALLOW_UNAUTHENTICATED_REMOTE=true), adds USER node, binds compose to 127.0.0.1:3002, and allowlists X-GitLab-API-URL against GITLAB_ALLOWED_HOSTS.
Then:
- Rotate the PAT. If ever network-reachable in SSE mode or run with
ENABLE_DYNAMIC_API_URL=true, treat it as compromised; reissue with narrowest scope. - Audit: GitLab access logs for unexpected token use, recent project uploads for an
environattachment, outbound requests to hosts that aren't your GitLab. - Bind to
127.0.0.1or front with an authenticating proxy; never expose 3002 routably. SetSSE_AUTH_TOKENfor remote SSE. - Leave
ENABLE_DYNAMIC_API_URLoff unless needed; if needed, setGITLAB_ALLOWED_HOSTSto exact trusted hostnames.
Caveats
The v2.1.27 fix for #1 does not sanitize upload_markdown's file_path — it only closes the unauthenticated door and shrinks blast radius, so the arbitrary-read primitive remains if auth is ever bypassed. Disclosure gap worth noting: Finding #1's fix shipped ~3.5 months after report, during which the vulnerable config stayed the default with no public advisory to prompt defenders.
Organisations named here
- CWE-918
- GHSA-2h44-8472-frjj
- GHSA-cv3r-c5h8-f4g5
Technologies named here
- @zereight/mcp-gitlab
- gitlab-mcp
- upload_markdown tool
- list_projects tool
- list_issues tool
- Model Context Protocol
- GitLab
- Docker
- npm
- Node.js
- CVSS
Techniques named here
- Exfiltration via legitimate service channel
- Confused deputy attack
- Path traversal
- SSRF
Related in AppSec
Flowise MCP Security Bypass Enables RCE
Inside an Oracle Database SQL Injection Attack | Huntress
OASIS
