{"id":"GHSA-5648-rgj9-v224","revision":null,"records":[{"revision":"d88383e65cdf4b50952cc66816db03e2","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-5648-rgj9-v224","aliases":[],"title":"@zereight/mcp-gitlab has multiple safety-control bypasses: execute_graphql read-only + allow-list bypass, unauthenticated transports, session-exhaustion DoS","description":"### 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).","state":0,"publishedAt":"2026-09-15T20:48:22+00:00","modifiedAt":"2026-09-15T21:00:08.4898154+00:00","affected":[{"ecosystem":"npm","name":"@zereight/mcp-gitlab","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"2.1.30"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/zereight/gitlab-mcp/security/advisories/GHSA-5648-rgj9-v224","https://github.com/zereight/gitlab-mcp/issues/596","https://github.com/zereight/gitlab-mcp/issues/748","https://github.com/zereight/gitlab-mcp/pull/571","https://github.com/zereight/gitlab-mcp/pull/624","https://github.com/zereight/gitlab-mcp/commit/69e784da33e96e64867b511c6f14d3f21f91ba8b","https://github.com/zereight/gitlab-mcp","https://github.com/zereight/gitlab-mcp/releases/tag/v2.1.30"],"sourceUrl":"https://github.com/advisories/GHSA-5648-rgj9-v224","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-5648-rgj9-v224","identifiers":["GHSA-5648-rgj9-v224"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-13T13:56:59.5128815+00:00","complete":true,"records":1779,"lastAttemptAt":"2026-11-13T13:56:59.5128815+00:00","lastSuccessAt":"2026-11-13T13:56:59.664403+00:00","error":null,"lastVerifiedAt":"2026-11-13T13:56:59.6644038+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-d8fa82e95fdb59244e2f635e667ba17f0796ca0a4f1f4bb8cc31febcd660b76c-171aaa8da54405fecd349043d7c6bca097805444deecfbdeb701916f8478ccac","complete":true,"records":425370,"lastAttemptAt":"2026-11-13T13:56:58.8285598+00:00","lastSuccessAt":"2026-11-13T13:56:58.73334+00:00","error":null,"lastVerifiedAt":"2026-11-13T13:56:58.8285598+00:00","snapshotPublishedAt":"2026-11-13T13:23:31+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-a75113f43b074d76b75fe921a3c1282c","complete":false,"records":13795,"lastAttemptAt":"2026-11-13T14:06:41.7937416+00:00","lastSuccessAt":"2026-11-13T14:06:40.774+00:00","error":"489 identifier lookups failed; latest failures include AccessDenied (HTTP 403). Other identifiers were checked independently.","lastVerifiedAt":"2026-11-13T14:06:40.774+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":12974,"expectedIdentifiers":13848,"failedIdentifiers":489},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-13T13:56:59.5128815+00:00","complete":true,"records":1123,"lastAttemptAt":"2026-11-13T13:56:59.5128815+00:00","lastSuccessAt":"2026-11-13T13:56:59.6652853+00:00","error":null,"lastVerifiedAt":"2026-11-13T13:56:59.6652855+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-1e24e6ff187942e0b34d6a0da935f8a7","complete":true,"records":13795,"lastAttemptAt":"2026-11-13T13:57:10.7854563+00:00","lastSuccessAt":"2026-11-13T13:57:10.781+00:00","error":null,"lastVerifiedAt":"2026-11-13T13:57:10.781+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13848,"expectedIdentifiers":13848,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-93e76d404e1aeb6a7e8902f5ffa06211fc86dea3b1958e297abb2668aa5e6e8c","complete":true,"records":9952,"lastAttemptAt":"2026-11-13T13:56:59.4676253+00:00","lastSuccessAt":"2026-11-12T23:19:15.5239119+00:00","error":null,"lastVerifiedAt":"2026-11-13T13:56:59.4676253+00:00","snapshotPublishedAt":"2026-11-12T23:03:02+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-4d60731753a2f72e455d1086e7203d0ef10843936dcc1cf2b45135e2caa204b0","complete":true,"records":2012,"lastAttemptAt":"2026-11-13T13:56:58.889439+00:00","lastSuccessAt":"2026-11-12T18:16:50.7862525+00:00","error":null,"lastVerifiedAt":"2026-11-13T13:56:58.889439+00:00","snapshotPublishedAt":"2026-11-12T17:48:11+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-ab4ad5f59937c0596645e186474e26d2fffbbe6ecc09983ad71f9a0d88918405","complete":true,"records":3129,"lastAttemptAt":"2026-11-13T13:56:59.4799748+00:00","lastSuccessAt":"2026-11-13T12:59:11.7864534+00:00","error":null,"lastVerifiedAt":"2026-11-13T13:56:59.4799748+00:00","snapshotPublishedAt":"2026-11-13T12:47:49+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-7d9437160a3664a4238cadabbe17e78db87dde648ca4c671841d50155dfa5d6c","complete":true,"records":231770,"lastAttemptAt":"2026-11-13T13:56:59.3642542+00:00","lastSuccessAt":"2026-11-13T12:59:06.1905777+00:00","error":null,"lastVerifiedAt":"2026-11-13T13:56:59.3642542+00:00","snapshotPublishedAt":"2026-11-13T12:33:13+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}