Docker's docker cp command carries a subtle but dangerous flaw: a time-of-check-to-time-of-use (TOCTOU) race condition in the path-resolution logic that is supposed to keep copy operations confined inside a container's filesystem. Tracked as CVE-2018-15664, the bug lets a malicious or compromised container win a timing race against the Docker daemon and trick it into reading or writing files on the host filesystem — with the daemon's own (typically root) privileges. In practical terms, an attacker who controls what's inside a container, or who can influence the timing of filesystem operations against it, can turn a routine docker cp invocation by a host operator or CI/CD pipeline into a full host compromise. It's a textbook example of how a security check that is correct at the moment it runs can still be bypassed if the underlying object changes state before the checked result is acted upon.
What's Actually Broken
docker cp needs to resolve paths safely so that copying files into or out of a container can't be abused to escape the container's root filesystem via symlinks. Docker's fix for that problem lived in a function called FollowSymlinkInScope, part of the pkg/archive/pkg/symlink code in the Moby (Docker Engine) project. The function walks a path component by component, resolving any symlinks it finds, and verifies that the fully resolved path stays inside the container's root directory before returning it as "safe."
The problem is what happens after that check. FollowSymlinkInScope returns a resolved, verified path — but it's just a string. The actual file open/copy happens in a separate step, referencing that path again by name. Between the verification and the actual file operation, nothing prevents the filesystem from changing. A process with write access inside the container (the container's own entrypoint process, or an attacker who has already gained code execution inside it) can replace a directory component along that path with a symlink pointing outside the container's root — for example, at /etc, /root/.ssh, or any other host path — in the narrow window between check and use.
Because the Docker daemon typically runs as root on the host, winning this race lets the attacker:
- Write attacker-controlled content to arbitrary host paths (persistence, backdoors, cron jobs, SSH keys)
- Read arbitrary host files when the race is exploited on a
docker cp <container>:<path> <host-dest>pull rather than a push - Escalate from "control over an untrusted container's contents" to "arbitrary file read/write on the container host"
Security researcher Aleksa Sarai — at the time a maintainer working closely on runc and Linux container internals — identified and reported the issue, framing it correctly as a race condition (CWE-362/CWE-367) rather than a simple path-traversal bug, since earlier, similar docker cp symlink escapes (CVE-2014-9356 and follow-ons) had already been patched with check-based validation. This bug demonstrated that check-then-use validation is fundamentally insufficient against a filesystem an attacker can mutate concurrently — only atomic, descriptor-based resolution closes the window for good.
Affected Versions and Components
- Docker Engine (Community and Enterprise editions) prior to 18.09.1 and prior to 18.06.2-ce are affected.
- The vulnerable code lives in the Moby project (
moby/moby), specifically the archive/symlink-resolution package used bydocker cp. It is not a runc or containerd vulnerability, though it's frequently confused with the unrelated (and more widely publicized) runc container-escape CVE-2019-5736 because both surfaced around the same period and both involve breaking the container/host boundary. - Downstream distributions that ship their own Docker packages — RHEL/CentOS, Ubuntu, Debian, SUSE — required their own backported builds; simply trusting an upstream "fixed" version number without confirming the OS vendor's package was rebuilt against the patched Moby code left systems exposed.
- Any tooling, CI/CD pipeline, or orchestration layer that shells out to
docker cpagainst containers built from untrusted, third-party, or community-supplied images/Dockerfiles inherits this exposure until the engine is patched.
CVSS, EPSS, and KEV Context
NVD rates CVE-2018-15664 as High severity, with a CVSS v3 base score in the 7.4 range. The scoring reflects a local attack vector (the attacker needs the ability to run code inside, or otherwise mutate the contents of, a container that the daemon will operate on), high attack complexity (the exploit depends on winning a race window, which is inherently non-deterministic and typically requires retries), and high confidentiality and integrity impact given the potential for arbitrary host file read/write as root.
The high attack complexity is the key reason this CVE never generated a large EPSS (Exploit Prediction Scoring System) probability or a mass-exploitation wave: EPSS scores for this CVE have historically sat low (well under 1%), and it does not appear on CISA's Known Exploited Vulnerabilities (KEV) catalog. That combination — high theoretical severity, low observed/predicted exploitation — is a common pattern for race-condition and TOCTOU bugs, and it's exactly the profile that causes vulnerability scanners relying on CVSS alone to either over-prioritize noisy findings or, worse, get deprioritized in patch queues because "nobody's exploiting it in the wild." Neither extreme is the right call in isolation; what matters is whether your environment actually runs docker cp against attacker-influenced containers, which is a reachability question, not a scanner question.
Timeline
- Mid-2018 — Aleksa Sarai reports the
docker cpTOCTOU symlink race to the Docker security team as a distinct issue from the previously patched path-traversal bugs, correctly identifying the root cause as an inherent check-then-use race rather than an insufficient validation check. - Late 2018 / early 2019 — Docker ships fixes across its supported release branches, landing in 18.06.2-ce and 18.09.1, reworking the symlink-resolution path to eliminate the race window.
- Early 2019 — CVE-2018-15664 is published with details of the flaw, its CWE classification, and the affected version ranges, alongside public writeups clarifying the "why check-then-use fails" lesson for container tooling generally.
- Ongoing — Downstream Linux distributions backport the fix into their own Docker packages on their own release cadences, creating a long tail of environments where the "upstream fixed" version number doesn't reflect the OS-packaged reality — a pattern that shows up in nearly every Docker/Moby CVE since.
Remediation Steps
- Upgrade Docker Engine to 18.09.1 or later (or 18.06.2-ce or later on the 18.06 branch), and for any currently supported Docker version, confirm your specific release includes the
pkg/symlinkfix rather than assuming version-number recency. - Verify OS-vendor packages, not just upstream tags. If you consume Docker via RHEL, Ubuntu, Debian, or SUSE repositories, confirm the distro's own changelog references CVE-2018-15664 as remediated in the installed package build — don't rely on the Moby release notes alone.
- Treat
docker cpas a privileged host operation, not a convenience command. Avoid running it against containers built from untrusted Dockerfiles, community base images, or any container whose filesystem contents an external party can influence — including in CI where build stages may pull unvetted dependencies. - Restrict Docker daemon access. Membership in the
dockergroup, or socket access, is root-equivalent. Reducing who and what can invokedocker cpagainst arbitrary containers shrinks the population of race-condition attempts that can ever be made. - Reduce blast radius with rootless Docker or user-namespace remapping where operationally feasible, so that even a successful race yields a compromised namespace rather than direct host root.
- Re-architect CI/CD artifact extraction. Pipelines that use
docker cpto pull build outputs out of containers are a common, overlooked exposure path. Prefer BuildKit's--outputexport mechanism or multi-stage build artifacts copied via the build graph itself, which don't traverse this legacy code path at all. - Fleet-wide verification. Don't rely on a single host check — inventory every Docker Engine instance across build fleets, self-hosted runners, and production hosts to confirm patched versions are actually deployed everywhere, not just where someone remembered to update.
How Safeguard Helps
This CVE is a good illustration of why CVSS-only prioritization gets teams into trouble: a High-severity score with low real-world exploitation likelihood is easy to either over-react to or bury, and both are wrong depending on your actual usage of docker cp. Safeguard's reachability analysis determines whether your build pipelines, base images, and running fleet actually invoke the vulnerable code path against untrusted container content, so you're not treating every legacy Docker Engine finding as an emergency by default. Griffin AI correlates that reachability signal with your SBOM data — generated automatically or ingested from existing scanners — to flag exactly which hosts, runners, and images are exposed to CVE-2018-15664-class TOCTOU issues rather than surfacing a blanket "Docker Engine outdated" alert across your entire estate. Where a fix is straightforward, such as bumping a pinned Docker Engine version in an infrastructure manifest or CI runner image, Safeguard opens an auto-fix PR so the remediation lands in code review rather than sitting in a backlog. And because race-condition and TOCTOU-class vulnerabilities keep recurring in container tooling, Safeguard's continuous SBOM monitoring means the next one gets flagged and reachability-scored the moment it's disclosed, not the next time someone happens to run a scan.