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의 프롬프트 템플릿 샌드박스를 벗어나 명령을 실행할 수 있었습니다. 자체 호스팅 운영자는 해당 GitLab 계열의 수정 버전으로 업데이트해야 합니다.[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 스테이징 환경에서 이뤄졌다고 설명합니다. 벤더 권고는 영향을 받는 모든 조건을 밝히지 않습니다.[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 역직렬화를 실행할 수 있었다고 보고합니다. 연구자가 기록한 경로이므로 모든 영향받는 flow나 벤더 패치를 완전히 설명한다고 추론하지 마세요.[3]

단계 4

Gateway에서 명령이 실행될 수 있음

확인된 영향은 AI Gateway 서버에서 임의 명령이 실행되는 것입니다. 단순한 모델 지시 수행 실패가 아닙니다.[1], [3]

소스 코드 비교: 취약한 구현 vs 보안 패치

GitLab 작업 항목 1850은 취약한 flow가 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)

엔지니어링 및 시스템 보안 강화 체크리스트

출처