A Jinja2 sandbox escape in GitLab's AI plumbing that turns a developer's prompt into root on the gateway box
CVE-2026-90970 is a server-side template injection (SSTI) flaw in the GitLab AI Gateway's Duo Workflow Service. An authenticated user who has been granted Duo Agent Platform access can craft a malicious flow configuration that breaks out of the prompt template sandbox — almost certainly a Jinja2 SandboxedEnvironment bypass — and gains arbitrary command execution on the AI Gateway host. Affected versions span AI Gateway 18.1.6 through 19.2.3, 19.3.0–19.3.1, and 19.4.0; fixed in 19.2.4, 19.3.2, and 19.4.1. Critically, only self-hosted AI Gateway deployments are vulnerable — organizations using GitLab.com, GitLab Dedicated, or the cloud-hosted AI Gateway are already patched by GitLab.
GitLab rates this CVSS 9.9 CRITICAL with Scope:Changed (sandbox → OS), and the technical chain — low-privilege auth, no user interaction, network-reachable, full CIA impact — justifies that vector *on paper*. In practice, the vendor score overstates urgency for most defenders. The vulnerability requires two gates: a valid GitLab account *and* admin-granted Duo Agent Platform access, which is a Premium/Ultimate add-on feature that many orgs have not yet rolled out. The affected population — self-hosted AI Gateway operators — is a small subset of an already smaller subset (self-managed GitLab customers who opted into self-hosting the AI proxy). This is the second SSTI in the same component in eight months (CVE-2026-1868 was patched in February), signaling a systemic template-handling weakness in the Duo Workflow Service that deserves architectural attention, but the real-world blast radius is narrower than the score implies.
4 steps from start to impact.
Authenticate to GitLab with Duo Agent Platform access
- Valid GitLab user credentials
- Duo Agent Platform access granted by admin
- Target org runs self-hosted AI Gateway (not cloud-hosted)
- Duo Agent Platform is an opt-in feature requiring admin enablement — many orgs have not rolled it out
- Self-hosted AI Gateway is a minority deployment model; most GitLab customers use the cloud-hosted gateway
- Requires insider access or compromised credentials as a starting position
Craft a malicious Duo Agent Platform flow configuration
SandboxedEnvironment. Common Jinja2 sandbox escapes use __subclasses__() traversal to reach os.popen() or subprocess.Popen. The payload is embedded within what appears to be a legitimate AI prompt template.- Knowledge of Jinja2 SSTI sandbox escape techniques
- Access to flow configuration interface
- No public PoC exists as of 2026-10-03, though Jinja2 SSTI is a well-documented attack class with abundant references
- The specific sandbox implementation may require targeted research to bypass — generic payloads may not work if GitLab added custom restrictions
{{, {%, __class__, __subclasses__) in flow definition fields; GitLab application logs for flow creation/modification eventsSubmit the flow to trigger template rendering on the AI Gateway
- AI Gateway processes the flow without additional input validation
- Template engine evaluates the payload server-side
- The gateway service likely runs as a non-root container user in Docker/Kubernetes deployments, limiting initial privilege
- Container isolation (if properly configured) constrains lateral movement from the gateway host
Exploit RCE for credential theft and lateral movement
- Gateway host stores model API credentials (environment variables or config files)
- Network connectivity from gateway to other internal services
- Container network policies and segmentation may block lateral movement
- Model API keys alone don't provide access to the GitLab instance or source code repos directly
- Modifying AI responses in transit requires deep understanding of the gateway's internal request pipeline
gitlab/model-gateway with the corresponding version tag) and redeploy. Per the noisgate mitigation SLA for HIGH severity, deploy within 30 days; given zero-day proximity and the CI/CD role, aim for 7 days if possible.- GitLab instance-level patching alone does not help — the vulnerability is in the AI Gateway service, which is a separate deployment artifact (
gitlab/model-gatewayDocker image). Patching GitLab CE/EE without updating the AI Gateway container leaves you exposed. - WAF rules for SSTI patterns are a partial mitigation at best — the flow configuration payload is submitted through GitLab's authenticated API, and template injection payloads can be obfuscated to bypass pattern-matching WAF rules. Do not rely on this as a primary control.
- Disabling GitLab Duo entirely would work but is operationally disruptive — if you've deployed self-hosted AI Gateway, your development teams likely depend on Duo features. Restricting Agent Platform access (not all Duo features) is the targeted control.
The supporting signals.
| In-the-wild exploitation | None confirmed. GitLab's advisory does not mention active exploitation. CISA assessed exploitation status as "none." Not listed in CISA KEV catalog as of 2026-10-03. |
|---|---|
| Proof-of-concept availability | No public PoC exists. Checked pocindex.io, GitHub (no repos named CVE-2026-90970), ExploitDB — all negative. However, Jinja2 SSTI sandbox escapes are a well-documented attack class with extensive public literature (e.g., HackTricks SSTI), so weaponization by a skilled attacker is realistic within days to weeks. |
| EPSS score | Not yet scored. CVE was published 2026-10-02; EPSS model has not yet computed a prediction. Expect initial score within 7–14 days. |
| CVSS vector interpretation | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H — Network-reachable, low complexity, low privilege, no user interaction, scope change (sandbox → OS), full CIA impact. The S:C is technically correct (template sandbox to host OS) but inflates the score; the PR:L underrepresents the *effective* privilege gate (Duo Agent Platform access is narrower than generic low-privilege auth). |
| Affected versions | AI Gateway 18.1.6 through 19.2.3, 19.3.0–19.3.1, 19.4.0 |
| Fixed versions | AI Gateway 19.2.4, 19.3.2, 19.4.1. GitLab CE/EE also patched in 19.2.4, 19.1.6, 19.0.8, 18.11.11. |
| Prior art | CVE-2026-1868 (Feb 2026, CVSS 9.9) — same CWE-1336, same component (Duo Workflow Service), same attack class. Second SSTI in the AI Gateway in eight months signals a systemic template-handling weakness. |
| Scanning / exposure data | No GreyNoise or Shodan tags specific to this CVE. Self-hosted AI Gateway instances are typically not directly internet-facing — they sit behind the GitLab instance and are accessed internally. Shodan/Censys exposure is expected to be minimal. |
| Researcher / disclosure | Reported by HackerOne user invisiblemeerkat via GitLab's bug bounty program. GitLab work item #628842. Coordinated disclosure on 2026-10-02. |
| Scope limitation | Only self-hosted AI Gateway deployments are affected. GitLab.com, GitLab Dedicated, and Self-Managed instances using the cloud-hosted AI Gateway are already patched. The vulnerable population is limited to orgs that explicitly chose to self-host their AI Gateway — primarily regulated industries (financial services, government, healthcare) with data residency requirements. |
Sources.
- The Hacker News — GitLab Patches Critical 9.9 AI Gateway Flaw
- SecurityOnline — GitLab AI Gateway Vulnerability CVE-2026-90970 Patched
- The Hacker Wire — CVE-2026-90970: SSTI Analysis & Fix
- SQ Magazine — GitLab AI Gateway Critical RCE CVE-2026-90970
- GitLab Docs — AI Gateway Configuration
- GitLab Docs — Duo Agent Platform Security
- GitLab Work Item #628842
- Strix.ai — CVE-2026-90970 Intelligence
Why this verdict
- Gate 1 — authentication + Duo Agent Platform access: The CVSS PR:L tag understates the real prerequisite. Exploiting this requires not just any GitLab account but one with admin-granted Duo Agent Platform access, which is a Premium/Ultimate add-on feature. This is meaningfully narrower than 'any low-privilege user' and implies either insider threat or post-compromise credential theft with specific privilege requirements.
- Gate 2 — self-hosted AI Gateway only: The vast majority of GitLab customers use the cloud-hosted AI Gateway (patched by GitLab centrally). Self-hosted AI Gateway is an opt-in deployment model chosen primarily by regulated enterprises. This reduces the vulnerable population to a small fraction of the GitLab installed base.
- No exploitation evidence or PoC: Disclosed 2026-10-02, no public PoC, no KEV listing, no confirmed exploitation. While Jinja2 SSTI is a known technique, the specific sandbox implementation requires targeted research. The urgency clock is ticking but has not yet reached the exploitation threshold.
- Role multiplier: The GitLab AI Gateway occupies a CI/CD-adjacent high-value role by definition — 100% of self-hosted AI Gateway installs serve CI/CD environments. RCE on the gateway enables: (1) theft of AI model provider API keys/credentials, (2) interception of code and prompts transiting the gateway, (3) potential supply-chain attack by modifying AI-generated code suggestions in transit, (4) lateral movement toward the GitLab instance. The blast radius at the high-value role is host → tenant → potentially supply-chain if the attacker modifies code suggestions. This sets a floor of HIGH. However, the AI Gateway is an *auxiliary AI proxy service*, not the core GitLab CI/CD engine (Runner, pipelines, registry). Supply-chain pivot requires additional steps beyond the initial RCE (understanding the gateway internals, intercepting the right request pipeline). The floor is HIGH, not CRITICAL.
- Scope:Changed inflation: The S:C in the CVSS vector reflects the sandbox → OS boundary crossing, which adds ~0.5 points. This is technically accurate but the 'sandbox' here is a template engine sandbox within a service that itself runs in a container — it's a scope change within a scope change, which the CVSS model doesn't distinguish from escaping a VM. Real-world impact is closer to an 8.5 than a 9.9.
Why not higher?
The floor for a canonical CI/CD component would be CRITICAL, but the AI Gateway is not the core CI/CD engine — it's an AI model proxy service. The supply-chain attack path (modifying AI code suggestions in transit) is plausible but requires significant additional effort beyond the initial RCE, and depends on developers blindly accepting AI-generated code. Upgrading to CRITICAL would require either confirmed exploitation, a public PoC demonstrating trivial weaponization, or evidence that the gateway runs with privileges sufficient to directly compromise the GitLab instance.
Why not lower?
Despite the friction, this is still unauthenticated-from-the-gateway's-perspective RCE on a service that handles sensitive code and AI model credentials. The insider-threat angle is real: a disgruntled developer with Duo access could pivot from 'I can write code' to 'I own the AI infrastructure.' The second SSTI in eight months in the same component suggests the underlying template handling is brittle and more bypasses may follow. Self-hosted AI Gateway deployments are concentrated in exactly the regulated, high-value environments where a compromise matters most.
Crowdsourced verification payload.
Run this script on the host running the AI Gateway container (or any host with Docker/kubectl access to inspect the gateway deployment). Execute as: bash check_cve_2026_90970.sh. Requires docker or kubectl CLI access. No elevated privileges needed beyond container inspection permissions.
#!/usr/bin/env bash
# check_cve_2026_90970.sh
# Checks whether the GitLab AI Gateway is vulnerable to CVE-2026-90970
# (SSTI sandbox escape in Duo Workflow Service)
# Outputs: VULNERABLE / PATCHED / UNKNOWN
set -euo pipefail
CVE="CVE-2026-90970"
COMPONENT="GitLab AI Gateway"
# Fixed versions
FIXED_192="19.2.4"
FIXED_193="19.3.2"
FIXED_194="19.4.1"
FIRST_VULN="18.1.6"
version_gte() {
# Returns 0 if $1 >= $2 (semver comparison)
printf '%s\n%s' "$2" "$1" | sort -t. -k1,1n -k2,2n -k3,3n -C 2>/dev/null
}
check_version() {
local ver="$1"
local major minor patch
IFS='.' read -r major minor patch <<< "$ver"
patch=${patch:-0}
# Check if version is below the first vulnerable version
if ! version_gte "$ver" "$FIRST_VULN"; then
echo "PATCHED"
echo "INFO: $COMPONENT version $ver is below the vulnerable range ($FIRST_VULN+)."
return
fi
# Check 19.4.x branch
if [[ "$major" -eq 19 && "$minor" -eq 4 ]]; then
if version_gte "$ver" "$FIXED_194"; then
echo "PATCHED"
echo "INFO: $COMPONENT version $ver >= $FIXED_194 (fixed)."
else
echo "VULNERABLE"
echo "DETAIL: $COMPONENT version $ver is vulnerable. Upgrade to >= $FIXED_194."
fi
return
fi
# Check 19.3.x branch
if [[ "$major" -eq 19 && "$minor" -eq 3 ]]; then
if version_gte "$ver" "$FIXED_193"; then
echo "PATCHED"
echo "INFO: $COMPONENT version $ver >= $FIXED_193 (fixed)."
else
echo "VULNERABLE"
echo "DETAIL: $COMPONENT version $ver is vulnerable. Upgrade to >= $FIXED_193."
fi
return
fi
# Check 18.1.6 through 19.2.x branch
if version_gte "$ver" "$FIRST_VULN"; then
if [[ "$major" -eq 19 && "$minor" -le 2 ]]; then
if version_gte "$ver" "$FIXED_192"; then
echo "PATCHED"
echo "INFO: $COMPONENT version $ver >= $FIXED_192 (fixed)."
else
echo "VULNERABLE"
echo "DETAIL: $COMPONENT version $ver is vulnerable. Upgrade to >= $FIXED_192."
fi
elif [[ "$major" -lt 19 ]]; then
echo "VULNERABLE"
echo "DETAIL: $COMPONENT version $ver is in the vulnerable range. Upgrade to >= $FIXED_192."
else
# 19.5+ assumed patched
echo "PATCHED"
echo "INFO: $COMPONENT version $ver is beyond the affected range."
fi
return
fi
}
echo "=== $CVE Vulnerability Check ==="
echo "Component: $COMPONENT"
echo "Checking for AI Gateway container..."
echo ""
GATEWAY_VERSION=""
# Method 1: Docker
if command -v docker &>/dev/null; then
CONTAINER_ID=$(docker ps --filter "ancestor=gitlab/model-gateway" --format "{{.ID}}" 2>/dev/null | head -1)
if [[ -z "$CONTAINER_ID" ]]; then
CONTAINER_ID=$(docker ps --filter "name=ai-gateway" --format "{{.ID}}" 2>/dev/null | head -1)
fi
if [[ -n "$CONTAINER_ID" ]]; then
IMAGE=$(docker inspect --format '{{.Config.Image}}' "$CONTAINER_ID" 2>/dev/null)
# Extract version from image tag (e.g., self-hosted-v19.2.3-ee or 19.2.3)
GATEWAY_VERSION=$(echo "$IMAGE" | grep -oP '\d+\.\d+\.\d+' | head -1)
echo "Found Docker container: $CONTAINER_ID"
echo "Image: $IMAGE"
fi
fi
# Method 2: Kubernetes
if [[ -z "$GATEWAY_VERSION" ]] && command -v kubectl &>/dev/null; then
POD=$(kubectl get pods --all-namespaces -l app=ai-gateway -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$POD" ]]; then
POD=$(kubectl get pods --all-namespaces -l app.kubernetes.io/name=ai-gateway -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
if [[ -n "$POD" ]]; then
NS=$(kubectl get pods --all-namespaces -l app=ai-gateway -o jsonpath='{.items[0].metadata.namespace}' 2>/dev/null || true)
IMAGE=$(kubectl get pod "$POD" -n "${NS:-default}" -o jsonpath='{.spec.containers[0].image}' 2>/dev/null || true)
GATEWAY_VERSION=$(echo "$IMAGE" | grep -oP '\d+\.\d+\.\d+' | head -1)
echo "Found Kubernetes pod: $POD (namespace: ${NS:-default})"
echo "Image: $IMAGE"
fi
fi
echo ""
if [[ -z "$GATEWAY_VERSION" ]]; then
echo "UNKNOWN"
echo "Could not detect a running GitLab AI Gateway container."
echo "If you know the version, re-run with: VERSION=19.2.3 $0"
# Allow manual override
if [[ -n "${VERSION:-}" ]]; then
GATEWAY_VERSION="$VERSION"
echo ""
echo "Using manually specified version: $GATEWAY_VERSION"
check_version "$GATEWAY_VERSION"
fi
exit 2
fi
echo "Detected version: $GATEWAY_VERSION"
echo ""
check_version "$GATEWAY_VERSION"