// uncategorised

GitLab’s AI Gateway Just Got a 9.9: When Your AI Flow Config Is a Shell

GitLab patched a 9.9 in its AI Gateway this week, and it is worth more than a “patch now” headline, because it is a clean example of the bug class that is going to define AI-agent security for the next few years. The short version: an authenticated user with access to the Duo Agent Platform could write a custom flow that broke out of the prompt template sandbox and ran arbitrary commands on a self-hosted gateway. Not jailbreak the model, not leak a prompt. Run commands on the box. CVE-2026-90970, CVSS 9.9, and the only reason it is not a 10 is the authentication requirement.

We break AI agents for a living, so here is the part the advisory does not spell out: why this keeps happening, what the attacker actually controls, and why the gateway is the single worst place in your stack to hand someone a shell.

What actually broke

GitLab’s AI Gateway is the service that sits between your GitLab instance and the model providers, and the Duo Agent Platform lets you define custom “flows,” reusable AI workflows you configure yourself. Under the hood those flows are rendered through a template engine, the same way a web framework renders a page from a template plus variables. That is the crack.

The vulnerability is classed CWE-1336, template injection. If you have done web testing you already know the shape, it is server-side template injection wearing an AI badge. The engine is built to interpolate values into a template string. The moment an attacker can smuggle template syntax into a field the engine evaluates, they are no longer supplying data, they are supplying code that runs in the template engine’s context. In most engines that context is one hop from the host: reach a built-in, walk the object graph to the runtime, and you have command execution. The “specially crafted flow configuration” in the advisory is exactly that: a flow definition with template directives in it, evaluated server-side, that escapes the sandbox GitLab wrapped around it.

Notice what is missing from that chain: the model. There is no clever prompt, no persuasion, no “ignore previous instructions.” The LLM is a bystander. The attacker is hitting the plumbing around the model, the config-rendering layer, which is ordinary application code with an AI-shaped input surface. That is the detail worth internalizing, because the whole industry is staring at the model and shipping injectable template engines behind it.

Why the gateway is the worst place to lose

A shell is bad anywhere. On this box it is catastrophic, and GitLab’s own advisory tells you why in one line: the self-hosted gateway stores JWT signing keys and holds connections to both the GitLab instance and the external AI providers. Walk through what that means for an attacker standing on it.

  • The JWT signing keys. If you hold the key that signs tokens, you do not steal a session, you mint them. Any identity, any time, valid by construction. That is the quiet upgrade from “RCE on one service” to “trusted across everything that validates these tokens.”
  • The line into the GitLab instance. The gateway is a trusted internal client. From there your reach is source code, CI/CD, secrets, pipeline runners, the usual crown jewels, approached from inside the trust boundary rather than from the login page.
  • The provider credentials. The gateway authenticates to the external model providers, so it holds those keys too. That is someone else’s compute on your bill, the METR-style token drain, and a data path out that looks like normal model traffic.

This is the lethal position in any agent architecture: a single component that is trusted by everything on one side and reaches everything on the other. Compromise it and you are not inside a service, you are inside the trust fabric. The gateway pattern is good engineering, it centralizes auth and policy, but centralized trust is also centralized blast radius, and this CVE is what cashing that out looks like.

This is a pattern, not an incident

Here is the part that should bother you. This is the second time this year. In February, GitLab patched CVE-2026-1868, also a 9.9, also a template injection through a crafted flow definition, that time landing as denial-of-service or code execution. Same component, same bug class, same entry point, eight months apart. One is a bug. Two is a surface.

And it is not just GitLab. The shape is everywhere AI features shipped fast in 2026: a config, a workflow, a “custom instruction” or a tool definition that users can author, rendered or evaluated server-side, with a sandbox bolted on afterward rather than a boundary designed in. The sandbox is the tell. You only need a sandbox around something you are feeding to an evaluator, and sandboxes around template and expression engines have been falling over since long before anyone put the letters A and I in front of them. The AI gateway inherited a 15-year-old web vulnerability class and a fresh, under-tested attack surface on top of it.

How you would actually find this

If you are testing an AI platform, this CVE is a map. Do not start at the chat box, start at everything a user can configure.

  • Enumerate the authoring surface. Every place a user defines a flow, an agent, a custom instruction, a tool schema, a prompt template, a webhook body. Anything you write that the platform later renders or evaluates is a candidate. That list is your test matrix, and it is almost never the part vendors threat-model hardest.
  • Probe for template evaluation, not model behavior. Drop classic SSTI polyglots into every configurable field and watch for evaluation: the arithmetic that comes back solved, the object that renders instead of printing literally. If a field reflects the result of an expression instead of the expression itself, you are inside the engine.
  • Then climb from evaluation to execution. Expression evaluation is the foothold, not the finding. The real question is how far the sandbox lets you walk toward a built-in, the runtime, the filesystem, a subprocess. Measure the escape, not the echo.
  • Map what the compromised component is trusted by. This is the step people skip and the one that turns a medium into a 9.9. Once you have execution on the gateway, inventory what it holds: signing keys, upstream credentials, internal connections. The impact is defined by trust reach, not by the bug itself.

If you run a self-hosted gateway

Patch, first and now. Self-hosted GitLab AI Gateway on 18.1.6 through 19.2.3 goes to 19.2.4, the 19.3.x line to 19.3.2, and 19.4.0 to 19.4.1. GitLab.com and Dedicated are already fixed, this is a self-hosted problem, and there is no workaround: the patch is the mitigation. invisiblemeerkat reported it through HackerOne, CISA listed no observed exploitation as of October 2, and you want to stay ahead of the window where that changes, because a public 9.9 with a clear bug class is a reliable magnet for n-day work.

But patching this CVE does not touch the pattern, and the pattern is what gets you next time. The lesson is architectural: treat every user-authored flow, template, and tool definition as untrusted code, because to a template engine that is exactly what it is. Do not render it in a context that can reach a runtime. And build the gateway on the assumption that it will eventually be compromised, so that when it is, the attacker inherits short-lived scoped credentials and a tight egress allowlist instead of your signing keys and a clear road to your source and your providers. A control in front of the gateway that constrains what it can reach and records what it did is the difference between “we rotated keys and moved on” and “we are reconstructing three weeks of a trusted component doing whatever it was told.”

Finding this class of flaw across the real authoring surface of an AI platform, before someone with a flow config does, is exactly what AI agent penetration testing and MCP server and tool-chain security testing are for.

// get started

Work with AgentOffense

Tell us about your target and goals. We’ll reply with scope and a fixed-price quote — usually within one business day.

./request_engagement