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