FlawOpen

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

● CVE-2026-66384 · 中危
安全研究 · FlawOpen

CVE-2026-66384:JFrog Artifactory Docker 缓存路径穿越

在特定远程仓库条件下,CVE-2026-66384 允许已认证用户在 JFrog Artifactory 预期的 Docker 缓存路径之外写入数据。请升级受影响的自托管实例。

💡 通俗易懂的原理解析 (ELI5)

想象仓库里的两个窗口用不同方式读取同一张标签:收货窗口从不可信的供应方取回包裹,入库窗口却把它放在可信产品的名称下。之后有人按可信名称取货时,可能拿到替代品。这个 Artifactory 案例中的缓存替换需要身份验证和特定远程仓库条件;它不代表任何已认证用户都能任意写入。

核心概念与专有名词

CWE-22
路径没有被可靠地限制在预期存储目录内。
Docker 远程缓存
保存从上游容器镜像仓库获取内容的仓库缓存。
已认证用户
已通过身份验证的用户。JFrog 还要求特定远程仓库条件;并非匿名访问。

框架映射

根本原因剖析 (Root Cause)

JFrog 将 CVE-2026-66384 归为 CWE-22,并说明在特定远程仓库条件下,已认证用户可能写入预期 Docker 缓存路径之外。OpenAI 技术报告提供了其关联到该 CVE 的特定事件机制:Artifactory 在选择本地缓存位置和解析上游注册表请求时,对同一精心构造的请求作出了不同解释。它从攻击者控制的仓库获取镜像清单和层,却以可信 CyberGym 镜像名称缓存这些内容。这是两种请求解释导致的缓存身份替换,并不能证明所有受影响部署都允许任意文件写入。OpenAI 确认缓存已被替换,但没有发现其他工作负载拉取或运行该替代镜像的证据。JFrog 未公开易受攻击的源码行或产品补丁差异;下面的代码仅演示路径限制,不是 JFrog 源码或漏洞复现。受影响版本为低于 7.146.35,以及 7.161.0 至 7.161.16;修复版本为 7.146.35 和 7.161.16。JFrog 表示受影响的 Cloud 环境已加固。严重性为 JFrog 评定的 Medium;本文不声称有数值 CVSS 评分。

攻击执行流程分解

步骤 1

已认证请求到达远程缓存

问题需要已认证用户和 JFrog 所述的特定远程仓库条件。

步骤 2

缓存路径边界失效

报告称,Artifactory 在选择本地缓存和解析上游注册表时对精心构造的请求作出了不同解释:它从攻击者控制的仓库取回镜像清单和层,却以可信 CyberGym 镜像名称存储。报告没有公开该请求本身。

步骤 3

影响范围以证据为准

之后请求可信镜像时,可能会收到被替换的内容。OpenAI 确认缓存遭到投毒,但没有发现其他工作负载拉取或运行该镜像的证据。在该评估环境中,执行还需要目标容器之后解析这个可信引用。

源代码对比:漏洞与安全实现

以下是说明性 Python 接口,并非 JFrog 源码。易受攻击的模式让不同解析器从同一个原始请求中分别得出缓存键和上游来源。加固模式只解析一个由服务器授权的不可变 RemoteImage,并在获取内容和确定缓存身份时重复使用同一对象。应从经过验证的配置构造该对象,将远程命名空间纳入缓存键;如果系统提供可信的不可变摘要,也应将内容与其绑定。这表达了报告所述的防御不变量,并非完整解析器、产品补丁或漏洞复现。请升级到 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)

工程与系统安全加固清单

参考来源