FlawOpen

flawopen.com/Teardowns/cve-2026-90970-gitlab-ai-gateway-prompt-template-sandbox-escape

● CVE-2026-90970 · CVSS 9.9[4] · Critical
FlawOpen Security Research

CVE-2026-90970: GitLab AI Gateway prompt sandbox escape

A specially crafted Duo Agent Platform flow could escape the AI Gateway prompt-template sandbox and execute commands. Self-hosted Gateway operators should upgrade to the fixed release for their GitLab line.[1], [4]

CVSS vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H[4]
💡 Plain English Explainer (ELI5)

A prompt template is like a clerk filling a report from a folder of conversation records. The clerk should copy approved fields as plain text. Here, a crafted template could tell the clerk to use the folder’s “restore” control, making the application rebuild live objects from stored data instead of simply copying words. That crosses the boundary between displaying data and asking the application to interpret it. The reporter documents the specific parse_raw()/pickle path; GitLab’s advisory confirms the sandbox escape and command-execution impact without describing every step.[3], [6]

Core Concepts & Subsystem Terms

AI Gateway
GitLab service that connects GitLab AI features to model providers; self-hosted installations have a separately versioned Gateway image.[5], [1]
Prompt template
A template that combines fixed instructions with runtime values, such as flow inputs and conversation context.[3], [6]
Sandbox escape
Using behavior available inside a restricted execution environment to cross the boundary it was meant to enforce.[3], [6]
CWE-1336
Improper neutralization of special elements used in a template engine; the CVE record assigns this weakness to the issue.[4], [1]

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-1336

Root Cause Analysis

GitLab's advisory confirms that, under certain conditions, an authenticated user with Duo Agent Platform access could use a specially crafted flow configuration to escape the prompt-template sandbox and execute arbitrary commands on the AI Gateway. It classifies the issue as CWE-1336 and rates it CVSS 9.9. GitLab's public work item #1850 gives a more specific reported path: a Jinja template invokes parse_raw() on a LangChain HumanMessage from conversation history, which permits pickle deserialization. The work item says this was demonstrated on GitLab staging; the vendor advisory does not specify all affected flow conditions. A public AI Gateway source commit dated 2026-09-17 adds a PromptSandboxedEnvironment, describes it as forbidding method access on bound objects, and hardens Jinja operations and filters. That change is mechanically consistent with restricting what a template can do with runtime objects, but the commit does not mention this CVE and public evidence reviewed does not map it to the fixed self-hosted images. Treat it as a separate public hardening change; no public source links it to this CVE or its fixed images.[1], [3], [4], [2]

Self-hosted AI Gateway fixed-release matrix

Deployment / release lineFixed AI Gateway releaseOperator action
Self-hosted GitLab 19.2 line19.2.4Upgrade to the matching self-hosted Gateway image tag and verify the deployed image.
Self-hosted GitLab 19.3 line19.3.2Upgrade to the matching self-hosted Gateway image tag and verify the deployed image.
Self-hosted GitLab 19.4 line19.4.1Upgrade to the matching self-hosted Gateway image tag and verify the deployed image.
GitLab-hosted AI GatewayGitLab states it was already patched.Confirm the deployment uses GitLab-hosted Gateway; do not apply self-hosted image guidance to this case.

Step-by-Step Attack Flow

Step 1

A user can configure a custom flow

The advisory's stated precondition is an authenticated user with Duo Agent Platform access. It does not name a more specific role or document every flow capability required.[1], [3]

Step 2

The flow supplies a crafted template configuration

A specially crafted flow configuration reaches the Gateway's prompt-template sandbox. The public advisory does not publish a safe, complete reproduction or the exact template expression.[1], [3]

Step 3

Template execution crosses the sandbox boundary

GitLab work item #1850 reports that a template could call parse_raw() on a LangChain HumanMessage in conversation history, triggering pickle deserialization. This is the reporter’s documented path; do not infer that it covers every affected flow or represents the vendor’s patch.[3]

Step 4

Commands can execute on the Gateway

The confirmed impact is arbitrary command execution on the AI Gateway. This is server-side execution on the Gateway, not merely an instruction-following failure in the model.[1], [3]

Source Code: Flaw vs. Secure Implementation

GitLab work item 1850 reports that the vulnerable flow exposed a LangChain HumanMessage object to Jinja, where a template could invoke parse_raw() and trigger pickle deserialization. This defensive example keeps live message objects out of the template context, passes only text data, and denies callable attribute and method access. It illustrates the boundary that must be closed; it is not GitLab’s released patch. GitLab’s separate public commit 89b72644 adds PromptSandboxedEnvironment and filter hardening; its actual title concerns Jinja block-set writes, and no public source links it to CVE-2026-90970 or the fixed images. Operators must follow the vendor’s fixed-release guidance.[3], [2], [6]

Source excerpt: ai_gateway/prompts/base.py (reported vulnerable formatter; hardened design is illustrative)

UNPATCHED FLAW
# Vulnerable-path excerpt reported in GitLab work item 1850.
from jinja2 import PackageLoader
from jinja2.sandbox import SandboxedEnvironment
from langchain_core.prompts.string import DEFAULT_FORMATTER_MAPPING
from typing import Any

jinja_loader = PackageLoader("ai_gateway.prompts", "definitions")
jinja_env = SandboxedEnvironment(loader=jinja_loader)

def jinja2_formatter(template: str, /, **kwargs: Any) -> str:
    return jinja_env.from_string(template).render(**kwargs)

DEFAULT_FORMATTER_MAPPING["jinja2"] = jinja2_formatter
HARDENED SECURE PATCH
# Illustrative defense pattern, not GitLab’s CVE patch.
from typing import Any
from jinja2 import StrictUndefined
from jinja2.sandbox import ImmutableSandboxedEnvironment

class DataOnlyPromptEnvironment(ImmutableSandboxedEnvironment):
    def is_safe_attribute(self, obj: Any, attr: str, value: Any) -> bool:
        if callable(value):
            return False
        return super().is_safe_attribute(obj, attr, value)

    def is_safe_callable(self, obj: Any) -> bool:
        return False

def render_prompt(template: str, history: list[Any]) -> str:
    safe_history = []
    for message in history:
        content = message.content
        if not isinstance(content, str):
            raise TypeError("prompt history must contain text only")
        safe_history.append({"content": content})

    env = DataOnlyPromptEnvironment(undefined=StrictUndefined)
    return env.from_string(template).render(history=safe_history)

Engineering & System Hardening Checklist

Sources