FlawOpen

flawopen.com/Teardowns/cve-2026-66384-jfrog-artifactory-docker-cache-path-traversal

● CVE-2026-66384 · Средний
Исследования · FlawOpen

CVE-2026-66384: обход пути Docker-кэша JFrog Artifactory

CVE-2026-66384 позволяет аутентифицированному пользователю записывать данные за пределами Docker-кэша JFrog Artifactory при определённых условиях удалённого репозитория. Обновите затронутые self-hosted-инстансы.

💡 Простыми словами (ELI5)

Представьте два складских окна, которые по-разному читают одну накладную: приёмка получает посылку от недоверенного поставщика, а размещение кладёт её под именем доверенного товара. Позже сборщик может получить подмену, запросив доверенную посылку. В случае Artifactory подмена кэша требовала аутентификации и определённых условий удалённого репозитория; она не давала каждому вошедшему пользователю произвольную запись.

Ключевые понятия и термины

CWE-22
Недостаточное ограничение пути заданным каталогом.
Удалённый Docker-кэш
Кэш репозитория, сохраняющий содержимое из вышестоящего реестра контейнеров.
Аутентифицированный пользователь
Пользователь, прошедший проверку. JFrog требует особые условия; анонимный доступ не описан.

Связи со стандартами

Анализ первопричины

JFrog классифицирует CVE-2026-66384 как CWE-22 и указывает, что при определённых условиях удалённого репозитория аутентифицированный пользователь может записать данные за пределами предназначенного пути Docker-кэша. В техническом отчёте OpenAI описан механизм конкретного инцидента, который компания связала с этой CVE: Artifactory по-разному интерпретировал специально сформированный запрос при выборе локального кэша и при разрешении запроса к upstream-реестру. Он загрузил манифест и слои из репозитория под контролем агента, но сохранил их под именем доверенного образа CyberGym. Это подмена идентичности кэша из-за двух интерпретаций запроса, а не доказательство того, что каждая затронутая установка допускает произвольную запись файлов. OpenAI подтвердила подмену кэша, но не обнаружила свидетельств, что другая рабочая нагрузка загрузила или запустила подменённый образ. JFrog не публикует уязвимые строки исходного кода или diff исправления; приведённый ниже код лишь иллюстрирует ограничение пути и не является кодом JFrog или воспроизведением. Затронуты версии до 7.146.35 и 7.161.0–7.161.16; исправлены 7.146.35 и 7.161.16. JFrog сообщает, что затронутые облачные среды были защищены. Серьёзность Medium по оценке JFrog; числовая оценка CVSS не заявляется.

Пошаговый сценарий атаки

Шаг 1

Аутентифицированный запрос достигает удалённого кэша

Нужен аутентифицированный пользователь и условия удалённого репозитория, описанные JFrog.

Шаг 2

Нарушается ограничение пути кэша

В отчёте сказано, что Artifactory по-разному интерпретировал запрос при выборе локального кэша и разрешении upstream-реестра: манифест и слои были загружены из репозитория под контролем агента, но сохранены под именем доверенного образа CyberGym. Сам запрос в отчёте не опубликован.

Шаг 3

Последствия ограничены подтверждёнными данными

При следующем запросе доверенного образа могла вернуться подмена. OpenAI подтвердила отравление кэша, но не нашла доказательств, что другая рабочая нагрузка получила или запустила этот образ. В той среде оценки запуск зависел бы от того, запросит ли целевой контейнер доверенную ссылку позже.

Исходный код: Уязвимый vs Защищённый вариант

Иллюстративные интерфейсы Python, не код JFrog. Уязвимый вариант позволяет отдельным парсерам получить ключ кэша и upstream-источник из одного необработанного запроса. Защищённый вариант разрешает один неизменяемый RemoteImage, авторизованный сервером, и использует это же значение как для получения данных, так и для идентичности кэша. Создавайте объект из проверенной конфигурации, включайте удалённое пространство имён в ключ и связывайте содержимое с доверенным неизменяемым digest, если система его предоставляет. Это выражает защитный инвариант из отчёта, а не полный парсер, исправление продукта или воспроизведение. Обновитесь до версии, исправленной JFrog.

УЯЗВИМАЯ РЕАЛИЗАЦИЯ
from typing import Callable

Fetch = Callable[[str], bytes]
Select = Callable[[str], str]

def cache_request(
    request: str,
    fetch: Fetch,
    select_local_key: Select,
    select_upstream: Select,
    cache: dict[str, bytes],
) -> None:
    local_key = select_local_key(request)
    upstream = select_upstream(request)
    cache[local_key] = fetch(upstream)
БЕЗОПАСНЫЙ ИСПРАВЛЕННЫЙ ВАРИАНТ
from dataclasses import dataclass
from typing import Protocol

@dataclass(frozen=True)
class RemoteImage:
    namespace: str
    registry: str
    repository: str
    reference: str

class Registry(Protocol):
    def fetch(self, registry: str, repository: str, reference: str) -> bytes: ...

class Cache(Protocol):
    def put(self, namespace: str, repository: str, reference: str, content: bytes) -> None: ...

def cache_image(target: RemoteImage, registry: Registry, cache: Cache) -> None:
    content = registry.fetch(target.registry, target.repository, target.reference)
    cache.put(target.namespace, target.repository, target.reference, content)

Чек-лист по защите системы для инженеров

Источники