GHSA-5648-rgj9-v224: @zereight/mcp gitlab Denial of Service Vulnerability

HIGH
CVSS 8.1
CWE-863
Published
Advisory Summary

GHSA-5648-rgj9-v224 is a HIGH (CVSS 8.1) denial of service vulnerability (CWE-863: Incorrect Authorization) affecting @zereight/mcp gitlab libraries in the npm ecosystem. The issue involves execute_graphql read-only + allow-list bypass, unauthenticated transports, session-exhaustion dos. Remediation is available by upgrading affected packages to version 2.1.30.

GHSA-5648-rgj9-v224 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-5648-rgj9-v224
Severity HIGH
CVSS v3.1 Base Score 8.1
CWE Classification CWE-863: Incorrect Authorization
Affected Product @zereight/mcp gitlab
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 8.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix GHSA-5648-rgj9-v224

To remediate this vulnerability, upgrade all affected @zereight/mcp gitlab dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@zereight/mcp-gitlab fixed: 2.1.30
npm install @zereight/mcp-gitlab@2.1.30
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @zereight/mcp-gitlab

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-5648-rgj9-v224, inspect your project dependencies using npm:

npm ls @zereight/mcp-gitlab

Analyze the terminal output to locate any version of @zereight/mcp gitlab in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.1.30), your application is affected.

Note: The npm ls @zereight/mcp-gitlab command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-863: Incorrect Authorization, where the product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. In @zereight/mcp gitlab, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary @zereight/mcp-gitlab exposes GitLab to an LLM agent while relying on read-only mode, a project allow-list, and transport auth as its safety controls. Five defects defeat those controls. Under the MCP threat model, tool-call arguments/content can be shaped by untrusted input (prompt injection) or a malicious client. Reviewed commit: 60adcc0de5b0e96c4c2029f7a25d2775946421d8 (package version 2.1.28). Source review only; PoCs are local/offline. - F1 (HIGH) execute_graphql defeats BOTH read-only mode and GITLAB_ALLOWED_PROJECT_IDS. - F2 (HIGH, deployment-conditional) Streamable HTTP /mcp unauthenticated under cookie-jar / device-flow credentials. - F3 (MEDIUM) SSE unauthenticated by default, no Origin/Host validation (DNS rebinding). - F4 (HIGH) unauthenticated session/transport-exhaustion DoS (token check is syntactic only). - F5 (LOW) CI job trace returned verbatim (prompt-injection surface). ### Details F1 — index.ts:9194-9245 (case "execute_graphql"), guard at :9196, detector in utils/graphql-query.ts. (a) Read-only bypass: graphqlQueryContainsWriteOperation() strips comments/strings then tests /(?:^|[};]\s*)(mutation|subscription)\b/. GraphQL treats commas as insignificant, and stripGraphQLCommentsAndStrings does not remove them, so a document beginning with `,mutation{...}` executes as a write but is classified read-only. (b) Allow-list bypass: the handler never calls getEffectiveProjectId() or rejectIfProjectScopedDeployment() (unlike other tools), so a raw GraphQL body reaches /api/graphql with the server token against any project the token can access, regardless of GITLAB_ALLOWED_PROJECT_IDS. execute_graphql is listed in readOnlyTools (tools/registry.ts:1288). F2 — index.ts:1048-1052 forces REMOTE_AUTHORIZATION/GITLAB_MCP_OAUTH only when started with a PAT or job token; hasCookie (:1029) and useOAuth (:1026) are absent from the gate. Started with --cookie-path or --use-oauth, validation passes and mcpBearerAuth degrades to next() (:12903). Every /mcp caller is unauthenticated while buildAuthHeaders() attaches the server's live session upstream. F3 — index.ts:12276 requireSseAuth is a pass-through when SSE_AUTH_TOKEN is unset (default), and the SSE transport (:12290) is created with no enableDnsRebindingProtection/allowedHosts/allowedOrigins. On the default loopback bind, a malicious web page can DNS-rebind to drive the local server with the operator's GitLab credentials. F4 — index.ts:12387 validateToken only checks length>=20 and charset (no upstream verification); parseAuthHeaders returns AuthData for any such string; new sessions are admitted purely on capacity (:12938, MAX_SESSIONS default 1000), and the per-session rate limiter only runs when a sessionId already exists. So garbage-token initialize floods fill all session slots for SESSION_TIMEOUT_SECONDS (default 3600s) -> 503 for legitimate users. F5 — index.ts:10898-10911 (get_pipeline_job_output) returns the CI job trace to the model verbatim. Job logs are attacker-influenceable (e.g. a fork MR pipeline), so embedded instructions become model context and, combined with F1, can escalate to writes. ### PoC (local, deterministic) F1 detector (replicates the shipped logic, no network): isWrite("mutation{deleteProject(input:{id:1}){errors}}") -> DETECTED isWrite(",mutation{deleteProject(input:{id:1}){errors}}") -> BYPASS (executes as a write under read-only mode) F2 (local GitLab + cookie file): STREAMABLE_HTTP=true GITLAB_AUTH_COOKIE_PATH=./cookies.txt GITLAB_API_URL=http://localhost:8080/api/v4 node build/index.js # starts without an auth error; from another shell with NO credentials: curl -s http://127.0.0.1:3002/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"poc","version":"0"}}}' F4 (REMOTE_AUTHORIZATION mode): for i in $(seq 1 1000); do curl -s -o /dev/null http://127.0.0.1:3002/mcp \ -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ -H 'Private-Token: aaaaaaaaaaaaaaaaaaaaaaaa' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"x","version":"0"}}}'; done # subsequent legitimate initialize -> 503 "Maximum 1000 concurrent sessions allowed" ### Impact A semi-trusted client or a prompt-injected agent can perform arbitrary GitLab writes while the operator believes the server is read-only, against projects outside the allow-list, up to the token's privileges (F1). Anyone able to reach the port (directly or via DNS rebinding on loopback) can use the server's GitLab credentials with no authentication (F2/F3). An unauthenticated attacker can deny service with ~1000 trivial requests (F4). Attacker-influenced CI logs can steer the agent (F5). ### Remediation Parse execute_graphql with a real GraphQL parser and reject non-query operations; enforce project scope or disable the tool under an allow-list (F1). Extend the startup gate to (hasToken || hasJobToken || hasCookie || useOAuth) and ship a mandatory Streamable-HTTP auth token (F2). Enable SDK DNS-rebinding protection with allowedHosts/allowedOrigins and require SSE_AUTH_TOKEN by default (F3). Validate tokens upstream before allocating a session, rate-limit new-session creation per IP, lower idle timeout (F4). Frame externally-sourced content as untrusted and cap size (F5).

GHSA-5648-rgj9-v224 Affected @zereight/mcp gitlab Versions

1 component(s)
Fixed Versions:
2.1.30
Product / Package Ecosystem Affected Versions Fixed Version
@zereight/mcp-gitlab
npm
< 2.1.30
2.1.30

References

Frequently Asked Questions

What is GHSA-5648-rgj9-v224?
GHSA-5648-rgj9-v224 is a security vulnerability in @zereight/mcp gitlab involving execute_graphql read-only + allow-list bypass, unauthenticated transports, session-exhaustion DoS under CWE-863: Incorrect Authorization.
Which @zereight/mcp gitlab versions are affected?
Affected versions include releases prior to 2.1.30 across affected packages: @zereight/mcp-gitlab.
What version fixes GHSA-5648-rgj9-v224?
Upgrading to 2.1.30 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @zereight/mcp-gitlab affected?
Yes, @zereight/mcp-gitlab is affected in releases prior to 2.1.30. Upgrade your dependency to 2.1.30 or later.
What is the CVSS score of GHSA-5648-rgj9-v224?
GHSA-5648-rgj9-v224 has a CVSS v3.1 base score of 8.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N.
What CWE is associated with GHSA-5648-rgj9-v224?
GHSA-5648-rgj9-v224 is classified under CWE-863: Incorrect Authorization (The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.).
How can I check npm dependencies for GHSA-5648-rgj9-v224?
Run the dependency tree command: 'npm ls @zereight/mcp-gitlab'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-5648-rgj9-v224",
  "modified" : "2026-09-15T20:48:22Z",
  "published" : "2026-09-15T20:48:22Z",
  "aliases" : [ ],
  "summary" : "@zereight/mcp-gitlab has multiple safety-control bypasses: execute_graphql read-only + allow-list bypass, unauthenticated transports, session-exhaustion DoS",
  "details" : "### Summary\n@zereight/mcp-gitlab exposes GitLab to an LLM agent while relying on read-only mode, a project allow-list, and transport auth as its safety controls. Five defects defeat those controls. Under the MCP threat model, tool-call arguments/content can be shaped by untrusted input (prompt injection) or a malicious client.\n\nReviewed commit: 60adcc0de5b0e96c4c2029f7a25d2775946421d8 (package version 2.1.28). Source review only; PoCs are local/offline.\n\n- F1 (HIGH) execute_graphql defeats BOTH read-only mode and GITLAB_ALLOWED_PROJECT_IDS.\n- F2 (HIGH, deployment-conditional) Streamable HTTP /mcp unauthenticated under cookie-jar / device-flow credentials.\n- F3 (MEDIUM) SSE unauthenticated by default, no Origin/Host validation (DNS rebinding).\n- F4 (HIGH) unauthenticated session/transport-exhaustion DoS (token check is syntactic only).\n- F5 (LOW) CI job trace returned verbatim (prompt-injection surface).\n\n### Details\n\nF1 — index.ts:9194-9245 (case \"execute_graphql\"), guard at :9196, detector in utils/graphql-query.ts.\n(a) Read-only bypass: graphqlQueryContainsWriteOperation() strips comments/strings then tests /(?:^|[};]\\s*)(mutation|subscription)\\b/. GraphQL treats commas as insignificant, and stripGraphQLCommentsAndStrings does not remove them, so a document beginning with `,mutation{...}` executes as a write but is classified read-only. (b) Allow-list bypass: the handler never calls getEffectiveProjectId() or rejectIfProjectScopedDeployment() (unlike other tools), so a raw GraphQL body reaches /api/graphql with the server token against any project the token can access, regardless of GITLAB_ALLOWED_PROJECT_IDS. execute_graphql is listed in readOnlyTools (tools/registry.ts:1288).\n\nF2 — index.ts:1048-1052 forces REMOTE_AUTHORIZATION/GITLAB_MCP_OAUTH only when started with a PAT or job token; hasCookie (:1029) and useOAuth (:1026) are absent from the gate. Started with --cookie-path or --use-oauth, validation passes and mcpBearerAuth degrades to next() (:12903). Every /mcp caller is unauthenticated while buildAuthHeaders() attaches the server's live session upstream.\n\nF3 — index.ts:12276 requireSseAuth is a pass-through when SSE_AUTH_TOKEN is unset (default), and the SSE transport (:12290) is created with no enableDnsRebindingProtection/allowedHosts/allowedOrigins. On the default loopback bind, a malicious web page can DNS-rebind to drive the local server with the operator's GitLab credentials.\n\nF4 — index.ts:12387 validateToken only checks length>=20 and charset (no upstream verification); parseAuthHeaders returns AuthData for any such string; new sessions are admitted purely on capacity (:12938, MAX_SESSIONS default 1000), and the per-session rate limiter only runs when a sessionId already exists. So garbage-token initialize floods fill all session slots for SESSION_TIMEOUT_SECONDS (default 3600s) -> 503 for legitimate users.\n\nF5 — index.ts:10898-10911 (get_pipeline_job_output) returns the CI job trace to the model verbatim. Job logs are attacker-influenceable (e.g. a fork MR pipeline), so embedded instructions become model context and, combined with F1, can escalate to writes.\n\n### PoC (local, deterministic)\n\nF1 detector (replicates the shipped logic, no network):\n    isWrite(\"mutation{deleteProject(input:{id:1}){errors}}\")   -> DETECTED\n    isWrite(\",mutation{deleteProject(input:{id:1}){errors}}\")  -> BYPASS (executes as a write under read-only mode)\n\nF2 (local GitLab + cookie file):\n    STREAMABLE_HTTP=true GITLAB_AUTH_COOKIE_PATH=./cookies.txt GITLAB_API_URL=http://localhost:8080/api/v4 node build/index.js\n    # starts without an auth error; from another shell with NO credentials:\n    curl -s http://127.0.0.1:3002/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \\\n      -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"poc\",\"version\":\"0\"}}}'\n\nF4 (REMOTE_AUTHORIZATION mode):\n    for i in $(seq 1 1000); do curl -s -o /dev/null http://127.0.0.1:3002/mcp \\\n      -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \\\n      -H 'Private-Token: aaaaaaaaaaaaaaaaaaaaaaaa' \\\n      -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"x\",\"version\":\"0\"}}}'; done\n    # subsequent legitimate initialize -> 503 \"Maximum 1000 concurrent sessions allowed\"\n\n### Impact\nA semi-trusted client or a prompt-injected agent can perform arbitrary GitLab writes while the operator believes the server is read-only, against projects outside the allow-list, up to the token's privileges (F1). Anyone able to reach the port (directly or via DNS rebinding on loopback) can use the server's GitLab credentials with no authentication (F2/F3). An unauthenticated attacker can deny service with ~1000 trivial requests (F4). Attacker-influenced CI logs can steer the agent (F5).\n\n### Remediation\nParse execute_graphql with a real GraphQL parser and reject non-query operations; enforce project scope or disable the tool under an allow-list (F1). Extend the startup gate to (hasToken || hasJobToken || hasCookie || useOAuth) and ship a mandatory Streamable-HTTP auth token (F2). Enable SDK DNS-rebinding protection with allowedHosts/allowedOrigins and require SSE_AUTH_TOKEN by default (F3). Validate tokens upstream before allocating a session, rate-limit new-session creation per IP, lower idle timeout (F4). Frame externally-sourced content as untrusted and cap size (F5).",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@zereight/mcp-gitlab"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.1.30"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/security/advisories/GHSA-5648-rgj9-v224"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/issues/596"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/issues/748"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/pull/571"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/pull/624"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/commit/69e784da33e96e64867b511c6f14d3f21f91ba8b"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/zereight/gitlab-mcp"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/zereight/gitlab-mcp/releases/tag/v2.1.30"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-863" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-15T20:48:22Z",
    "nvd_published_at" : null
  }
}