GitLab AI Gateway 的 CVSS 9.9:当提示词沙箱本身成了攻击面

·阅读约9分钟·Evergreen Tools Team

GitLab 在 2026 年 10 月初披露了自托管 AI Gateway 中的一个严重漏洞,编号 CVE-2026-90970,由 GitLab 作为 CNA 给出 CVSS 3.1 的 9.9 分(满分 10)。描述比分数更重要:一个拥有 Duo Agent Platform 权限的已认证用户,可以用特制的 flow 配置逃出提示词模板沙箱,最终在 AI Gateway 本身实现任意命令执行。受影响区间为:18.1.6 起至 19.2.4(不含)之前的所有版本、19.3 至 19.3.2 之前、19.4 至 19.4.1 之前。

屏幕上的安全代码流

沙箱只有被当作边界测试过,才算边界

把访问链讲明白

把这段 CVE 描述当成一条序列来读,因为序列才告诉你该做什么。攻击者先得是一个已认证用户,并拥有 Duo Agent Platform 权限;然后提交一份特制的 flow 配置;提示词模板引擎没有正确中性化特殊元素,于是特制输入逃出了模板沙箱;一旦在沙箱外,它就能触达底层操作系统,在 AI Gateway 主机上实现任意命令执行。链条里没有任何一环是奇技淫巧:它就是一个带权限前置条件的模板注入,最后却拿到了严重性量表上第二高的分数。

# Know what you are running before you read the rest of this.
# Affected ranges, per the CVE record (GitLab Inc. as CNA):
#   AI Gateway 18.1.x-19.2.x : 18.1.6 up to (not including) 19.2.4
#   19.3.x                   : before 19.3.2
#   19.4.x                   : before 19.4.1
import re

AFFECTED = [(18, 1, 6, 19, 2, 4), (19, 3, 0, 19, 3, 2), (19, 4, 0, 19, 4, 1)]

def parse(v):
    m = re.match(r'(\d+)\.(\d+)\.(\d+)', v)
    return tuple(int(x) for x in m.groups()) if m else None

def is_affected(version):
    t = parse(version)
    if t is None:
        return None
    for lo_a, lo_b, lo_c, hi_a, hi_b, hi_c in AFFECTED:
        if (lo_a, lo_b, lo_c) <= t < (hi_a, hi_b, hi_c):
            return True
    return False

# Enumerate every deployment, including the ones nobody remembers.
print(is_affected('19.2.3'))   # True - still in the affected range

CWE-1336 就是全部故事

CWE 归类是 CWE-1336:模板引擎中特殊元素未被正确中性化。这既不是新类别,也不是机器学习问题。它和过去二十年里打穿无数 Web 框架的漏洞属于同一类:系统接受用户输入,把它喂给某个会求值或插值的东西,并且在「输入只是数据」的假设下运行——而这份输入其实是代码。让这一例特别有意思的细节在于它的标签:这个引擎存在的目的恰恰是隔离用户编写的 flow,也就是说,那个「负责兜住不可信输入」的组件,本身就是那个没能兜住不可信输入的组件。

# The access chain is the defence plan. Break one link, close the path.
#   authenticated user -> Duo Agent Platform permission
#   -> crafted flow -> sandbox escape -> command execution
CHAIN = {
    'authenticated_user':      'enforce SSO + MFA; no shared bot accounts',
    'duo_agent_platform_perm': 'grant to the smallest group that needs it',
    'crafted_flow':            'no public self-service flow authoring',
    'sandbox_escape':          'patch to a fixed version',
    'command_execution':       'least-privilege service account, egress deny',
}

def residual_risk(controls_in_place):
    open_links = [k for k in CHAIN if k not in controls_in_place]
    return {'open_links': open_links,
            'note': 'patching is the only link that fully closes this one'}

# Lateral movement is the part nobody plans for: what can the gateway
# reach? Model credentials, other repos, the database it brokers for.
机房里的机架服务器

网关位于每一次 AI 辅助评审的路径上

为什么影响范围大于一个仓库

AI Gateway 是 GitLab Duo(AI 编码助手与 Agent 套件)底下的算力层。它中转那些喂给语言模型集成的提示词流,并且跑在自托管实例上。那里的一个命令执行立足点,不是某一个仓库的立足点:每一次 AI 辅助的建议、评审与自动化 flow,都要经过那台机器。值得对自己部署问的问题是那些关于凭据与可达范围的问题:网关握着哪些模型 API Key、它还能跟哪些服务对话、它需要的数据库访问有多宽、攻击者拿到这三样东西会干什么。最后这份清单才是真正的损失评估所在。

# Look for the shape of the exploit in your own logs, not the payload.
# The CWE here is CWE-1336: template engine, special elements not neutralised.
import re, json

SUSPICIOUS = [
    re.compile(r'\$\{'),          # template interpolation
    re.compile(r'\{\{'),          # handlebars / jinja style
    re.compile(r'<%'),             # ERB style
    re.compile(r'__\w+__'),       # dunder reach-through
    re.compile(r'\bimport\b|\brequire\b|\bos\b\.'),
]

def flag_flow(entry):
    blob = json.dumps(entry, ensure_ascii=False)
    hits = [p.pattern for p in SUSPICIOUS if p.search(blob)]
    return {'user': entry.get('user'), 'flow_id': entry.get('flow_id'),
            'signals': hits, 'action': 'review' if hits else 'none'}

# This finds attempts, not proofs. It is triage, not a conviction.

防守方清单

先升级到修复版本——按你的发布线是 19.2.4、19.3.2 或 19.4.1——并且从正在跑的服务上确认版本号,而不是从变更日志里确认。然后削减权限前置:到底谁真的需要 Duo Agent Platform 访问权,这个组还能更小吗?然后限制「逃逸后的进程能做什么」:最小权限的服务账号、受限的出网、别把长期有效的模型凭据放在网关的环境里。最后看 flow 编写面:如果任何已认证用户都能编写 flow,那对一个本该被隔离的功能来说,这就是一个很大的输入面。

# The durable fix: never render user-supplied templates with a
# general-purpose engine. Validate against a schema instead.
from string import Template

ALLOWED = {'repo_name', 'branch', 'max_files'}   # exhaustive, by design

def render_flow(template: str, params: dict) -> str:
    unknown = set(params) - ALLOWED
    if unknown:
        raise ValueError(f'rejecting flow: unknown params {sorted(unknown)}')
    for k, v in params.items():
        if not isinstance(v, (str, int)):
            raise TypeError(f'{k} must be a scalar')
        if isinstance(v, str) and any(c in v for c in '{}<>'):
            raise ValueError(f'{k} contains template metacharacters')
    return Template(template).safe_substitute(**params)

# safe_substitute + an exhaustive allowlist + no evaluation. If a
string can be evaluated, it will be.
电路板特写

切断链条上的任何一环,攻击路径就断了

该记什么日志,以及日志做不到什么

把 flow 编写事件连同用户身份、flow 标识与完整配置记下来。留意模板元字符——插值分隔符、双下划线「触达」模式、shell 调用动词——把它们当作分诊信号,而不是证据。要清楚这是事后检测:当你在日志里看到特制 flow 时,它已经被提交了。检测告诉你「有没有被针对」并给你范围,它不能替代边界。该 CVE 的公开通告里没有包含 PoC 利用代码,而为了验证一个「直接打补丁就能解决」的漏洞去重建一份 PoC,并不值得。

# Prove the patch landed and prove the boundary holds. Both.
#!/usr/bin/env bash
set -euo pipefail
AI_GATEWAY="${AI_GATEWAY:-https://gitlab.example.com}"
TOKEN="${DUO_TOKEN:?set DUO_TOKEN}"

ver=$(curl -sf -H "Authorization: Bearer $TOKEN" \
        "$AI_GATEWAY/api/v4/ai_gateway/version" | python3 -c 'import sys,json;print(json.load(sys.stdin)["version"])')
echo "AI Gateway version: $ver"

case "$ver" in
  19.2.[4-9]*|19.2.[1-9][0-9]*|19.[3-9].*|2[0-9].*) echo 'version is at or above a fixed line' ;;
  *) echo 'STILL AFFECTED - patch before doing anything else'; exit 1 ;;
esac

grep -c 'prompt_template' /var/log/gitlab/ai-gateway/current || true
# Version checks are necessary and they are not sufficient. Check the
# boundary too: can the gateway process spawn a shell at all?

能长期成立的工程教训

永远不要用通用引擎去渲染用户提供的模板。这不是关于这个 CVE 或这家厂商的规则,而是关于「可被求值的文本」的规则。如果某种配置格式需要参数化,就给它一份穷尽式的可替换名白名单,只接受标量,拒绝含模板元字符的输入,并且优先使用「不求值」的替换调用——模板与 eval 之间的区别,就是表单与远程代码执行原语之间的区别。把这条规则写进开发者会读到的地方,因为下一个提示词沙箱,会由一个没听过这个 CVE 的人来写。

📌 常见问题 FAQ

CVE-2026-90970 是什么?

GitLab AI Gateway 组件中的一个严重漏洞。按其 CNA 的描述,一个拥有 Duo Agent Platform 权限的已认证用户,可以通过特制的 flow 配置逃出提示词模板沙箱,导致在 AI Gateway 上实现任意命令执行。

严重程度如何?

GitLab Inc. 作为 CNA 给出 CVSS 3.1 基础分 9.9,向量为 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H。CWE 归类为 CWE-1336(模板引擎中特殊元素未被正确中性化)。

哪些版本受影响?

按 CVE 记录:AI Gateway 从 18.1.6 起、至 19.2.4(不含)之前的所有版本;19.3 线中 19.3.2 之前的版本;19.4 线中 19.4.1 之前的版本。升级前请在 GitLab 通告上按你的发布线确认修复版本。

攻击者需要凭据吗?

需要。攻击者必须已经是拥有 Duo Agent Platform 访问权的已认证用户。这让它更偏向权限提升与沙箱逃逸,而不是一个对公网匿名开放的漏洞;这也让权限卫生成为真正的缓解措施,而不只是形式。

能从日志里检测吗?

部分可以。flow 编写事件应记录用户身份、flow 标识与完整配置,这些配置里出现的模板元字符值得分诊。但这里的检测是事后的:特制 flow 出现在日志里时,它已被提交。请把日志分析当作划定范围的手段,而不是预防手段。