Vulnerability DatabaseGHSA-rxjw-584q-hxjr

GHSA-rxjw-584q-hxjr: 
JavaScript vulnerability analysis and mitigation

Summary

A tool argument that is placed into the path of a request to GitLab was not checked for path separators or dot segments. A crafted value could make the server build a request path that, once a front proxy or GitLab resolves the dot segments, names a different GitLab API endpoint than the action intends. The request still carries the user's own token and never exceeds the authority that token already has, but it can reach an endpoint the action was not meant to touch, and it bypasses the server's own per-action controls (the tool exclusion list, the tier filter, the chosen tool surface, and the destructive-action confirmation). The attacker in the threat model is a model filling tool arguments under prompt injection: the model, not the operator, chooses the crafted value.

Affected versions

The defect is present in the whole release history. The fix lands in the maintained 3.x line only; the 1.x and 2.x lines are unmaintained and receive no backport (see the security policy's supported versions).

EcosystemPackageVulnerablePatched
Gogithub.com/jmrplens/gitlab-mcp-server<= 1.6.1none
Gogithub.com/jmrplens/gitlab-mcp-server/v2<= 2.7.5none
Gogithub.com/jmrplens/gitlab-mcp-server/v3<= 3.3.03.4.0
npm@jmrp.io/gitlab-mcp-server<= 3.3.03.4.0
PyPIjmrplens-gitlab-mcp-server<= 3.3.03.4.0
NuGetgitlab-mcp-server<= 3.3.03.4.0
The range was established by running the reproduction against the tree of every
release tag for the job artifact and system hook URL-variable actions: the job
artifact actions are affected from the first release (1.0.0), and the system
hook URL-variable actions from the release that added them (2.0.2). The other
actions listed below were not swept per tag, and their first affected release
comes from the source history: for the first class, the release whose handler
first called the same client-go route; for the second class, the first release
(1.0.0), whose model registry route and transport already left a value that is
. or .. as a whole a dot segment. All are still present on the development
branch and refused only by the fix.

Impact

The server itself unconditionally builds a request whose path does not correspond to the action the model invoked. Whether that reaches a different GitLab endpoint depends on what sits in front of GitLab:

  • On a standard install, GitLab's own Gitlab::Middleware::PathTraversalCheck decodes the request path and, from GitLab 18.2, answers a path-traversal attempt with 400 by default. The middleware acts only while its check_path_traversal_middleware feature flag is on, and before 18.2 that flag is off by default, so the middleware neither logs nor refuses the attempt. Switched on, it refuses on 18.0 and 18.1; on 17.4 to 17.11 it refuses only with its check_path_traversal_middleware_reject_requests flag on as well, and otherwise only logs, as it does from 16.5, which added it, to 17.3. Where it does not refuse, neither Grape nor Rails strips dot segments, so the request reaches the route it names literally, or none, and is answered 404. In both cases the attacker gains nothing.
  • Behind a front layer that resolves dot segments before forwarding the request (some ingress, proxy or CDN configurations normalize the path), the normalized path reaches Grape and the request is served at the resolved endpoint under the user's token. On a read action this can return the body of another endpoint the token may read, back to the model as if it were the action's result; on a write action it can direct a PUT or DELETE to another endpoint the token may write. The request never exceeds the token's own scope and grants, so GitLab continues to enforce authorization; the impact scales with the authority of the configured token and is largest for an administrator token behind a normalizing front layer. The distinct harm is that the server bypasses the operator's and the model's per-action controls and acts on an endpoint the approved action never named.

Affected actions

Two classes of route are affected. Each action below says how it was confirmed: through the real handlers and the real client transport chain against a mock GitLab that records the request (mock), and for some also live, through the real 3.3.0 binary against a licensed GitLab Enterprise Edition 19.5 instance, both directly and behind a proxy that resolves dot segments (live). The fix was run against the mock and, live, behind the proxy. Beyond its named action, the second class was confirmed on a sample. Routes that place the argument into the path without escaping it. Each of these actions passes a path argument to a client-go route that interpolates it into the request path as it is, so the value can carry separators and dot segments into the path:

  • Reproduced (mock) on every release tag from the one that added it, and on the development branch: job.download_single_artifact, job.download_single_artifact_by_ref, job.download_artifacts, admin.system_hook_set_url_variable and admin.system_hook_delete_url_variable.
  • Reproduced (mock) on the development branch: group.hook_set_url_variable, group.hook_set_custom_header, pipeline.schedule_delete_variable, template.license_get and template.project_template_get.
  • Reproduced (mock and live) on 3.3.0: group.hook_delete_url_variable, group.hook_delete_custom_header, group.hook_test, pipeline.schedule_edit_variable and template.project_template_list. On 3.3.0 each sent the dot segment to the wire; the fix refuses each at the handler, naming the parameter, with no request sent. Live, behind the proxy, 3.3.0 sent each of those last five actions with an argument that is .. as a whole, and GitLab received it at another path under the user's token: the URL-variable and custom-header deletions deleted the whole group webhook, the schedule variable edit reached the update route of the schedule itself, which reads none of the fields it sent, the template list read the project, and the hook test reached the hook's own path, where no route answered; sent directly, GitLab refused each with 400, and the fix sent none of them. Routes that escape the argument, where a value that is exactly . or .. still changes the route. Escaping keeps a separator inside the value, but a value that is . or .. as a whole is still a dot segment once it is in the path:
  • model_registry.download (the model_version_id, filename and path arguments): its route escapes each argument but leaves a dot as it is, as the route requires. From 3.3.0 the handler normalizes path with path.Clean before sending it, which resolves a . or .. between slashes. path.Clean splits on / alone, so a dot segment in path marked off by a backslash is kept as a name and sent with the backslash escaped as %5C; a layer that decodes it and reads a backslash as a separator still sees the dot segment, so path stays affected on 3.3.0 in that form. Reproduced (mock): model_version_id and filename reached the wire as a dot segment on 3.3.0; a path that is . or .. as a whole reached it on 3.2.0, before path.Clean, and on 3.3.0 only the backslash form still carries one (a percent-encoded one goes out escaped twice); the fix refuses each at the handler. Not run live, since the proxy used does not read a backslash as a separator.
  • Any other action whose route escapes a path argument: client-go writes a dot in such an argument as %2E, and this server's own transport turned it back into . before sending (see Root cause), so a value that is . or .. as a whole reached the wire as a dot segment. Reproduced (mock) on a sample of five actions across three domains, reads and writes (branch.get, tag.get, release.get, branch.delete, tag.delete), and live for branch.get and tag.get: on 3.3.0 each sent the dot segment to the wire, and the fix's transport check refused each before any request. The sample stands for the class, which is every escaped path argument; the transport check covers all of them. Live, behind the same proxy, branch.get and tag.get with a name that is

. as a whole were served as the project's whole branch or tag list, which the 3.3.0 handler then failed to read as a single entry; sent directly, GitLab answered 400 and 404, and the fix sent neither. A value that is . or .. as a whole is one segment, so it can only remove its own place in the route and, for .., the segment before it; it adds no segment of its own. The backslash form of the model registry path can carry several dot segments and a name after them. Reads, available to a normal user with read access:

  • job.download_single_artifact and job.download_single_artifact_by_ref (the artifact_path argument) and job.download_artifacts (the ref_name argument).
  • template.license_get, template.project_template_get and template.project_template_list (the template name and type arguments).
  • model_registry.download (the arguments named above).
  • In the second class, any read action whose route escapes a path argument. Writes, each bounded by the role the action already requires:
  • admin.system_hook_set_url_variable and admin.system_hook_delete_url_variable (the key argument): require an administrator token.
  • group.hook_set_url_variable, group.hook_delete_url_variable, group.hook_set_custom_header, group.hook_delete_custom_header and group.hook_test (the key and trigger arguments): require the Owner role on the group, and are served only on a Premium or Ultimate instance.
  • pipeline.schedule_edit_variable and pipeline.schedule_delete_variable (the key argument): require the schedule's owner or a Maintainer.
  • In the second class, any write action whose route escapes a path argument, at the role that action already requires. The first class has one root cause: a path argument reaches a client-go route that interpolates it into the request path without escaping it. The second has two: escaping that leaves a dot as it is, in the model registry route, and this server's own rewrite of %2E to ., everywhere else.

Root cause

A few client-go routes build the request path by interpolating a path argument without escaping it (client-go's NoEscape path variable), so a value carrying a separator or a dot segment survives into the outgoing path. The server did not neutralize such a value before the request was built, and the server's own transport also rewrites %2E to . (a workaround for some proxies), so an encoded dot segment reaches the wire as a literal one. The same rewrite undoes client-go's escaping of a dot in every escaped argument, so an argument that is . or .. as a whole reaches the wire as a dot segment as well, and the model registry route escapes its arguments in a way that never encodes the dot. The client-go behavior is reported to GitLab privately.

Fix

The patch adds one central check, a check in each affected handler, and escaping of the affected arguments:

  • A check in the GitLab transport chain, through which every surface and every handler reaches GitLab, refuses to send any request whose path carries a . or .. segment, in the path as it stands or once its percent-encoding is undone. That includes an escaped argument that is . or .. as a whole, so the check covers the second class on every action. The check applies to every action, including any added later, and refuses rather than rewriting the request. The refusal is reported to the model as a refused path argument that never left the server, not as a network or TLS failure.
  • Every handler of the first class, and the model registry download, checks each such argument before any request is built and refuses a . or .. segment with an error that names the parameter. A dot inside a segment, as in v1.2, is still accepted. In the model registry path, a . or .. between slashes is still resolved within the package, and a dot segment the normalization keeps as a name, marked off by a backslash or written percent-encoded, is refused the same way.
  • The handlers of the first class also escape each such argument before passing it to client-go: as one segment where the route expects one name, and segment by segment, keeping its /, for an artifact path and the archive ref. A separator in a single name therefore stays inside that name. A value with a space, a non-ASCII byte or a quote no longer strips the encoding from the project or group path it is sent with.

Workarounds

Until 3.4.0, each of these stops the part it names. Only the first covers both classes on every action; the third covers every action named above. Each variable is given as its release line reads it: GITLAB_MCP_* on 3.x, and the names given below on 1.x and 2.x.

  • At GitLab. Have Gitlab::Middleware::PathTraversalCheck refuse such a path with 400, with no layer in front of GitLab that resolves dot segments before forwarding. It does so by default from GitLab 18.2, as long as its check_path_traversal_middleware feature flag stays on; on 18.0 and 18.1, turn that flag on; on 17.4 to 17.11, turn on both it and check_path_traversal_middleware_reject_requests. From 16.5, which added the check, to 17.3 it can only log. A layer that resolves dot segments first hides them from that check, so this holds only without one.
  • Read-only mode. GITLAB_MCP_READ_ONLY=true on 3.x, or GITLAB_READ_ONLY=true on 1.x and 2.x, removes every write action, among them the nine above (admin.system_hook_set_url_variable, admin.system_hook_delete_url_variable, the five group.hook_* actions and the two pipeline.schedule_*_variable actions), and with them the writes of the second class. It does not cover the seven read actions (the three job.* actions, the three template.* actions and model_registry.download) or the reads of the second class.
  • Excluding the actions. On 3.x, GITLAB_MCP_EXCLUDE_TOOLS takes canonical action IDs, comma-separated, and this value removes all sixteen actions named above on any surface:
    GITLAB_MCP_EXCLUDE_TOOLS=job.download_single_artifact,job.download_single_artifact_by_ref,job.download_artifacts,template.license_get,template.project_template_get,template.project_template_list,model_registry.download,admin.system_hook_set_url_variable,admin.system_hook_delete_url_variable,group.hook_set_url_variable,group.hook_delete_url_variable,group.hook_set_custom_header,group.hook_delete_custom_header,group.hook_test,pipeline.schedule_edit_variable,pipeline.schedule_delete_variable
    Append them to any value already set, since the variable is read as one list. On 2.x, and on 1.x from 1.0.6, the variable is EXCLUDE_TOOLS, and it takes the names of the tools the server registers rather than action IDs: on the individual surface it removes these actions by that release's own tool names, and on the meta surface, or the dynamic surface where the release has one, only together with the domain tool that carries them, such as gitlab_job or gitlab_group. 1.0.0 to 1.0.5 read no such variable.
  • A token without administrator access. GitLab then refuses the two admin.system_hook_* actions whatever their path. This covers that family only: the job artifact, template, model registry, group hook and pipeline schedule actions, and the second class, stay reachable at the roles listed above.

Severity

CVSS v3.1 base vector CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N, base score 4.7 (Medium). Reasoning:

  • AV:N: the server is reached over its transport and relays to GitLab over the network.
  • AC:H: reaching a different endpoint depends on a condition outside the attacker's control: a front layer that normalizes the path before forwarding. A stock install does not serve the attempt (400 from GitLab 18.2, where the path-traversal check is on by default, 404 before it).
  • PR:N: this is indirect prompt injection. Whoever plants the content the model later reads (an issue, a comment, a file) holds no privilege on this server or on GitLab that the attack relies on: the request rides the victim's configured token, not the planter's.
  • UI:R: the attack does not act on its own. A victim has to have the model act over content the attacker controls, and only then is the crafted argument filled and sent.
  • S:C: the vulnerable component is this server. The impacted resource is a GitLab API resource, and GitLab's own authorization decides who may read or change it. That is a different security authority from this server's. The confidentiality and integrity impact below falls on that GitLab resource. Bypassing this server's own per-action controls is part of the vulnerability, but it is not what justifies the scope change.
  • C:L / I:L / A:N: bounded reads (the job artifact, template and model registry download actions) and bounded writes (the system hook, group hook and pipeline schedule variable actions), always within the token's scope; no availability impact. Sensitivity: scored for a deployment where the fronting layer normalizes unconditionally (AC:L), the vector is

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N, base score 6.1 (Medium). Both land in Medium.

CWE

  • CWE-23: Relative Path Traversal
  • CWE-20: Improper Input Validation (CWE-441, Unintended Proxy or Intermediary, is an alternative framing of the "argument chooses the endpoint" effect.)

Credit

Found in the project's own October 2026 security review.


Source: NVD

Related JavaScript vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-v25x-53wq-6r99HIGH8.2
  • JavaScript logoJavaScript
  • mcp-sanitizer
NoYesOct 31, 2026
GHSA-grx6-75c5-f738HIGH7.1
  • JavaScript logoJavaScript
  • compressing
NoYesOct 31, 2026
GHSA-qvp2-8q5w-gcfpMEDIUM6.5
  • JavaScript logoJavaScript
  • mcp-sanitizer
NoYesOct 31, 2026
GHSA-vh7f-3c46-hfp3LOWN/A
  • JavaScript logoJavaScript
  • cron-parser
NoNoOct 31, 2026
GHSA-f7wr-f2gj-v69xLOWN/A
  • JavaScript logoJavaScript
  • seroval
NoYesOct 31, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Additional Wiz resources

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management