FlawOpen

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

● CVE-2026-90970 · CVSS 9.9[4] · 緊急
セキュリティ研究 · FlawOpen

CVE-2026-90970:GitLab AI Gateway のプロンプトサンドボックス脱出

細工された Duo Agent Platform フローは AI Gateway のプロンプトテンプレート用サンドボックスを脱出し、コマンドを実行できる可能性がありました。自前運用の場合は対応する修正版へ更新してください。[1], [4]

CVSS ベクトルCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H[4]
💡 わかりやすい解説 (ELI5)

会話記録をもとに報告書を作る担当者を想像してください。許可された項目は通常のテキストとして転記するはずです。しかし細工されたテンプレートは、担当者に記録の「復元」操作を使わせ、単語を写す代わりに保存データからアプリケーション内で使えるオブジェクトを再構築させることができました。これはデータの表示と、アプリケーションにデータを解釈させることの境界を越えます。研究者の報告には parse_raw() と pickle を使う具体的な経路が記載され、GitLabの勧告はサンドボックス脱出とコマンド実行の影響を確認していますが、すべての手順は説明していません。[3], [6]

主要な概念と専門用語

AI Gateway
GitLab の AI 機能とモデル提供元を接続するサービス。自前運用 Gateway イメージは別にバージョン管理します。[5], [1]
Prompt template
固定指示とフロー入力や会話コンテキストなど実行時の値を組み合わせるテンプレート。[3], [6]
Sandbox escape
制限環境内の動作を利用し、本来強制されるべき境界を越えること。[3], [6]
CWE-1336
テンプレートエンジンの特殊要素の無効化が不適切であること。この弱点に CVE レコードが分類しています。[4], [1]

フレームワーク対応

  • このガイドで説明する弱点の CWE 参照: CWE-1336

根本原因の分析 (Root Cause)

GitLab によると、特定条件下で Duo Agent Platform にアクセスできる認証済みユーザーは、細工された設定でプロンプトテンプレート用サンドボックスを脱出し、AI Gateway 上で任意のコマンドを実行できる可能性がありました。CWE-1336、CVSS 9.9 です。条件や脆弱な関数の詳細は公開されていません。2026-09-17 の公開コミットは PromptSandboxedEnvironment を追加し、バインドされたオブジェクトのメソッドアクセスを禁止すると説明し、Jinja の操作やフィルターを強化します。これは実行時オブジェクトへの操作を制限する対策と合いますが、この CVE には触れておらず、修正版イメージとの対応も公開情報で確認できません。独立した公開強化変更として説明してください。公開情報ではこのCVEや修正版イメージとの関連は確認されていません。 GitLab の公開作業項目 1850 には、より具体的な経路が記載されています。Jinja テンプレートが会話履歴内の LangChain HumanMessage に parse_raw() を呼び出し、pickle のデシリアライズを起動します。この実証は報告者によるもので GitLab の staging 環境で行われたと記されていますが、ベンダーの勧告は影響条件のすべてを説明していません。[1], [3], [4], [2]

セルフホスト AI Gateway の修正リリース一覧

デプロイ / リリース系列修正済み AI Gateway バージョン運用担当者の対応
セルフホスト GitLab 19.2 系列19.2.4対応するセルフホスト Gateway イメージタグに更新し、稼働中のイメージを確認する。
セルフホスト GitLab 19.3 系列19.3.2対応するセルフホスト Gateway イメージタグに更新し、稼働中のイメージを確認する。
セルフホスト GitLab 19.4 系列19.4.1対応するセルフホスト Gateway イメージタグに更新し、稼働中のイメージを確認する。
GitLab ホスト型 AI GatewayGitLab は修正済みと説明しています。GitLab ホスト型 Gateway を使っていることを確認する。この場合、セルフホスト用イメージの手順は適用しない。

ステップ・バイ・ステップの攻撃フロー

ステップ 1

ユーザーがカスタムフローを設定できる

前提は認証済みで Duo Agent Platform にアクセスできることです。具体的なロールや必要な機能すべては公開されていません。[1], [3]

ステップ 2

フローから細工されたテンプレート設定が渡される

細工された設定が Gateway のプロンプトテンプレート用サンドボックスに到達します。公開情報に安全な完全再現手順や正確な式はありません。[1], [3]

ステップ 3

テンプレート実行がサンドボックス境界を越える

GitLab の公開作業項目 1850 は、テンプレートが会話履歴内の LangChain HumanMessage に parse_raw() を呼び出して pickle のデシリアライズを起動できたと報告しています。これは研究者が記録した経路です。影響を受ける全フローやベンダーのパッチと同一視しないでください。[3]

ステップ 4

Gateway 上でコマンドが実行される可能性がある

確認済みの影響は AI Gateway 上の任意コマンド実行です。モデルが指示に従わないだけでなく、Gateway サーバー上での実行です。[1], [3]

ソースコード比較:脆弱 vs 堅牢化

GitLab の作業項目 1850 によると、脆弱なフローは LangChain HumanMessage オブジェクトを Jinja に渡し、テンプレートから parse_raw() を呼び出して pickle のデシリアライズを起動できました。この防御例ではメッセージオブジェクトをテンプレートのコンテキストに渡さず、テキストだけを渡し、呼び出し可能な属性やメソッドへのアクセスを拒否します。これは閉じるべき境界を示す例であり、GitLab が公開したパッチではありません。公開コミット 89b72644 は独立したセキュリティ変更です。実際のタイトルは Jinja block-set の書き込みを扱っており、公開情報では CVE-2026-90970 や修正版イメージとの関連は確認されていません。運用者はベンダーが案内する修正版を適用してください。[3], [2], [6]

ソース抜粋: ai_gateway/prompts/base.py(報告された脆弱なフォーマッター。防御例)

脆弱な実装
# GitLab の作業項目 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
堅牢化されたセキュアパッチ
# これは防御パターンの例であり、このCVEに対するGitLabの修正パッチではありません。
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)

エンジニアリング&システム堅牢化チェックリスト

参考資料