FlawOpen

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

● CVE-2026-66384 · Medium
FlawOpen Security Research

CVE-2026-66384: JFrog Artifactory Docker Cache Path Traversal

CVE-2026-66384 lets an authenticated user write outside the intended JFrog Artifactory Docker cache under specific remote-repository conditions. Upgrade affected self-hosted instances.

💡 Plain English Explainer (ELI5)

Imagine two warehouse desks read the same parcel label differently: the receiving desk fetches a parcel from an untrusted supplier, while the storage desk files it under a trusted product name. A later picker can then receive the substitute when asking for the trusted parcel. In this Artifactory case, the documented cache substitution required authentication and specific remote-repository conditions; it did not give every authenticated user arbitrary write access.

Core Concepts & Subsystem Terms

CWE-22
Improper limitation of a pathname to a restricted directory: a path is not reliably kept inside its intended storage boundary.
Docker remote cache
Repository cache that stores content fetched from an upstream container registry.
Authenticated user
A user who passed authentication. JFrog also requires specific remote-repository conditions; anonymous access is not described.

Framework mappings

Root Cause Analysis

JFrog classifies CVE-2026-66384 as CWE-22 and says an authenticated user may write outside the intended Docker cache path under specific remote-repository conditions. OpenAI’s technical report supplies the incident-specific mechanism it linked to this CVE: Artifactory interpreted a crafted request differently when selecting the local cache location and resolving the upstream registry request. It fetched a manifest and layers from an attacker-controlled repository, then cached them under a trusted CyberGym image name. That is a cache-identity substitution across two request interpretations, not evidence that every affected deployment can write arbitrary files. OpenAI confirmed the cache substitution but found no evidence another workload pulled or ran the substituted image. JFrog does not publish vulnerable source lines or a product patch diff, so the code below is an illustrative path-containment control, not JFrog source or a reproduction. Affected versions are before 7.146.35 and 7.161.0 through 7.161.16; fixed versions are 7.146.35 and 7.161.16. JFrog says affected Cloud environments were fortified. Severity is Medium per JFrog; no numerical CVSS score is asserted.

Step-by-Step Attack Flow

Step 1

Authenticated request reaches remote cache

The issue requires an authenticated user and JFrog’s specific remote-repository conditions.

Step 2

Cache path boundary fails

The report says Artifactory interpreted the crafted request differently for local cache selection and upstream registry resolution: it fetched a manifest and layers from an attacker-controlled repository but stored them under a trusted CyberGym image name. The report does not publish the request itself.

Step 3

Impact remains bounded by evidence

A later request for the trusted image could have received the substituted content. OpenAI confirmed that the cache was poisoned, but found no evidence another workload pulled or executed that image. In that evaluation environment, execution would have depended on a target container later resolving the trusted reference.

Source Code: Flaw vs. Secure Implementation

Representative Python interfaces, not JFrog source. The vulnerable pattern lets separate parsers derive the cache key and upstream source from one raw request. The hardened pattern resolves one immutable, server-authorized RemoteImage and uses that same value for both fetch and cache identity. Build that object from validated configuration, include the remote namespace in the cache key, and bind content to a trusted immutable digest where the system provides one. This states the defensive invariant described by the incident report; it is not a complete parser, a product patch, or a reproduction. Upgrade to a JFrog-fixed release.

UNPATCHED FLAW
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)
HARDENED SECURE PATCH
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)

Engineering & System Hardening Checklist

Sources