@zereight/mcp-gitlab: DNS rebinding reaches local Streamable HTTP MCP transport
@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:
// index.ts:12077
app.use(express.json());
registerDownloadProxy(app);
The Streamable HTTP transport is then created without the SDK DNS-rebinding controls:
// 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:
// 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
initializesucceeds with attacker-controlledHostandOrigin; - no-token
tools/listis rejected with401; - the same forged-origin flow with a planted
Private-Tokenlists tools and callslist_project_variables; - the fake GitLab API records the forwarded token and returns a planted fake project variable.
Start the fake GitLab API:
python3 -