CopyEscape: Taking Over Docker Hosts with docker cp

Imperva Red Team uncovered CVE-2026-17106, a container-to-host arbitrary file-write vulnerability in Docker’s docker cp command. Docker later confirmed that the same CVE also affected sbx cp when copying files out of Docker Sandboxes. A malicious container or sandbox could exploit the copy process to create or overwrite files outside the destination selected by the user, potentially enabling code execution on the machine running the Docker CLI.

Through CVE-2026-17106, an attacker-controlled container could turn a routine file copy into two powerful outcomes:

Host file overwrite: creating or replacing any file writable by the user running docker cp, including shell configuration, executables, SSH configuration, source code, and persistence mechanisms.

Code execution: Executing attacker-controlled code as the logged-in user on macOS or, when docker cp runs with elevated privileges on Linux, achieving root code execution immediately after the command completes by overwriting the runc binary.

The exploit chains two weaknesses in Docker’s archive pipeline: a filesystem race that lets a running container produce an inconsistent tar archive, and an extraction flaw that allows the Docker CLI to follow a planted symlink outside the intended destination.

Together, these weaknesses turn the user’s own Docker CLI into a write primitive. The attacker controls the container and its files, the CLI supplies access to the host filesystem.

The docker cp command is routinely used to retrieve build artifacts, test results, logs, and forensic evidence from containers. This makes the vulnerability particularly relevant to CI systems, developer workstations, privileged automation, and incident-response workflows. In the last case, simply collecting evidence from a compromised container could activate the payload waiting inside it.

The impact also reaches AI-agent workflows. Docker Sandboxes uses sbx cp to move files between an isolated agent environment and the host. Docker Sandboxes 0.38.0, released August 6, identifies and fixes a “destination-escape flaw in sbx cp copy-out” under CVE-2026-17106. This means retrieving an artifact produced inside an untrusted or compromised coding-agent sandbox could expose the host to the same class of destination escape.

From a Simple Copy to a Host Write

The command that started this research could hardly look more ordinary:

docker cp container:/path/to/file.txt ./file.txt

The source is inside the container. The destination is chosen by the user. The expected security boundary seems obvious: Docker should write to ./file.txt and nowhere else.

But docker cp is not a direct filesystem-to-filesystem copy.

When copying from a container, the Docker daemon walks the container’s live filesystem and turns the selected path into a tar archive. The Docker CLI receives that archive and extracts it on the client machine.

image3

This architecture creates a powerful connection between two security domains. The container controls the archive contents, while the local CLI performs the resulting filesystem operations with the permissions of the person or process that invoked docker cp.

For this design to remain safe, Docker needs to preserve two properties:

  1. The daemon must produce a coherent archive from the container’s filesystem.
  2. The CLI must confine every extracted object to the destination selected by the user.

We found a way to break both properties in the same copy operation.

Impact

The vulnerability provides an arbitrary file create or overwrite primitive within the permissions of the Docker CLI process.

The malicious container does not automatically inherit root privileges from dockerd, and exploitation requires a user or automated system to invoke docker cp against the attacker-controlled path.

But the final write occurs on the machine running the CLI, with the authority of that user-not with the permissions of the process inside the container.

We validated the exploit on Linux with Docker Engine 29.6.1 and on macOS with Docker Desktop 4.81.0 (232925).

Developer Compromise on macOS

Docker Desktop runs the daemon inside a Linux virtual machine, but a container-to-local docker cp operation is extracted by the CLI running on macOS.

This means the archive crosses the VM boundary before the vulnerable operation occurs. The symlink is followed by a process on the Mac, and its target is interpreted in the Mac’s filesystem namespace.

A malicious container could therefore target files such as shell startup scripts, user-level executables, SSH configuration, source trees, cloud configuration, or persistence mechanisms under ~/Library/LaunchAgents. Overwriting a shell startup file could execute the attacker’s code the next time the user opens a terminal.

The container does not need to escape the Docker Desktop VM directly. It convinces the local CLI to perform the write on its behalf.

Root Code Execution on Linux

On Linux, the daemon and CLI commonly run on the same host. The write still carries the CLI user’s permissions.

For an ordinary user, the attacker can reach files writable by that account. If an administrator, CI worker, or maintenance process invokes sudo docker cp, the operation can reach root-owned system paths.

In our proof of concept, we replaced /usr/bin/runc with a shell script. A later Docker lifecycle operation executed the replaced binary and created a marker file as root.

The exploit did not obtain root from the Docker daemon. The copy already had enough authority to replace runc, and the later execution converted that file write into root code execution.

The Archive That Described Two Filesystems

The first part of the chain was in the archive producer.

Both the Moby and Docker CLI source trees we reviewed used github.com/moby/go-archive v0.2.0. For a container-to-local copy, Moby’s containerArchivePath creates a tarballer and runs it inside the container filesystem:

image13

Source: Moby containerArchivePath, Docker Engine 29.6.1, lines 76–82.

Docker locks the container object while the archive is read, but that lock protects Docker’s internal state. It does not freeze processes running inside the container.

Those processes can continue renaming files, replacing directories, and creating symlinks while the daemon walks the source tree.

The tarballer performs that walk with filepath.WalkDir:

image8

Source: moby/go-archive v0.2.0 archive walk, lines 691–802.

WalkDir reads a directory entry and records its type. If the entry is a directory, the walker plans to descend into it.

The callback then passes the pathname to addTarFile, which looks it up again with Lstat to create the tar header:

image12

Source: moby/go-archive v0.2.0 addTarFile, lines 295–317.

The same pathname now answers two different questions at two different times:

Traversal decision: is this entry a directory that WalkDir should enter?

Archive metadata: is this entry a file, directory, or symlink that should be written to the tar stream?

If the path changes between those observations, the two answers no longer need to agree.

Turning the Race into a Reliable Primitive

In our proof of concept, /watched/file.txt is intentionally deceptive. An LD_PRELOAD interposer makes it appear to be a regular file to processes inside the container by redirecting exact accesses to a hidden backing file, /watched/.file.txt.regular. The Docker daemon, however, sees the underlying file.txt path as a directory and can walk the attacker-controlled tree beneath it.

The walker initially sees the following directory:

image14

After WalkDir records escape as a directory, the attacker moves it aside and replaces it with a staged symlink:

image7

The switch requires only two rename operations, winning a filesystem race blindly would be unreliable, so we gave the container a clock.

Our proof of concept places a large file immediately before the pivot directory and monitors it with filesystem notifications. When the daemon opens that file, the attacker knows the walk has reached the right location. The large file widens the timing window, and the notification signals when to perform the two renames.

What begins as a race becomes a repeatable sequence.

After the switch, addTarFile sees a symlink and emits a symlink header for file.txt/escape. Control then returns to WalkDir, whose earlier directory entry still says that escape is a directory. The walker descends through the same pathname and adds a child beneath the symlink’s archive name.

The resulting tar stream contains two entries:

image5

Each entry is understandable on its own. Together, they describe two different moments in the source filesystem: escape is represented as a symlink, while its child is represented as though escape were still a directory.

image11

At this point, the container has produced an inconsistent archive. That alone should not be enough to write outside the destination. The client still controls extraction and can reject the dangerous sequence.

It did not.

The Extractor Checked One Path and Created Another

For a copy from a container, the Docker CLI eventually reaches archive.CopyTo:

image6

Source: moby/go-archive v0.2.0 CopyTo, lines 418–437.

Neither option confines traversal through an intermediate symlink. The vulnerable symlink handling attempted to perform its own containment check:

image1

Source: moby/go-archive v0.2.0 symlink extraction, lines 480–492.

The validation and the filesystem operation use different values.

The containment check validates targetPath, a path constructed with filepath.Join. The subsequent os.Symlink call receives the original hdr.Linkname from the archive.

For an extraction path of /safe/output/file.txt/escape and a link target of /usr/bin, the validation evaluates:

image2

That constructed path appears to remain under /safe/output, so the check approves it. Docker then gives the original absolute target to the kernel:

image4

The check approves one path. The filesystem creates another.

The same condition has an additional weakness: string prefixes do not represent path containment. A path such as /safe/output-elsewhere also begins with /safe/output. Our chain already had what it needed from the checked-value-versus-used-value mismatch.

The extractor then processes the child entry, file.txt/escape/runc.

Docker cleans the archive name, joins it to the destination, and checks for a lexical ../ escape, but the child entry contains no ../. As text, it remains beneath the destination.

The previous tar entry has changed what the pathname means on disk. Before creating the child, the vulnerable extractor does not re-establish confinement against the resolved target:

image10

Source: moby/go-archive v0.2.0 addTarFile, lines 295–317.

When the file is created, the kernel follows escape -> /usr/bin. The attacker’s bytes land at /usr/bin/runc on the Docker client rather than under /safe/output.

The full chain is:

  1. WalkDir records escape as a directory.
  2. The container replaces it with an absolute symlink.
  3. addTarFile emits the symlink into the tar stream.
  4. WalkDir descends using its earlier directory type and emits a child entry.
  5. The CLI validates a constructed target but creates the original absolute symlink.
  6. The CLI extracts the child through that symlink and writes outside the destination.

One stale directory entry and one ineffective containment check were enough to turn docker cp into a host write.

From Host Write to Host Compromise

At this point, the copy operation is no longer confined to the destination chosen by the user. The attacker-controlled container has gained a write primitive on the Docker client, with the permissions of the process running docker cp.

What happens next depends on those permissions. A copy launched by a developer could target user-writable startup files, shell configuration, source code, or persistence mechanisms. If the command runs as root, as is common in automation and some Linux environments, the same primitive could reach system executables and privileged configuration, turning the file write into root code execution.

The vulnerability simply prepares the malicious filesystem and waits. The trigger may be a developer retrieving a build artifact, a CI job collecting test results, or an incident responder copying evidence from a container they already suspect is compromised.

In each case, the operation intended to take data safely out of the container becomes the step that carries the attack back onto the host.

Mitigations

Users should upgrade to Docker Engine and CLI 29.7.2 or later, and Docker Desktop 4.86.0 or later.

Where upgrading is not immediately possible:

  • Avoid copying from running, untrusted, or compromised containers onto a sensitive client.
  • Stop the container before copying. Docker supports copying from a stopped container, preventing a live process from performing the race used in this chain.
  • Avoid sudo docker cp and root-run copy automation.
  • Run the Docker CLI with the least privilege required for the workflow.
  • Retrieve data from suspicious containers using a disposable account, VM, or similarly isolated environment.

Stopping the container blocks the producer race demonstrated here. It does not replace safe extraction: any archive originating from an untrusted source should still be treated as hostile input.

Conclusion

Container boundaries are not defined only by namespaces, virtual machines, or the Docker daemon. They also depend on the client-side tools that move data across those boundaries.

docker cp connects a live, attacker-controlled filesystem to an extractor running with the user’s authority. That design requires both sides of the pipeline to preserve a strict contract: the archive must be coherent, and every extracted object must remain inside the destination.

CVE-2026-17106 broke that contract. The daemon observed one pathname as two different objects, producing a symlink followed by its apparent child. The CLI then validated one path, created another, and allowed the child entry to cross onto the client filesystem.

The most important lesson is simple: archive extraction is a security boundary. Path normalization and string-prefix checks cannot enforce that boundary once symlinks and concurrent filesystem changes are involved. Filesystem operations must be rooted so that the kernel, not a string comparison, guarantees where the write can land.

We responsibly disclosed the vulnerability to Docker and delayed publication several times to align disclosure with a usable patch. We would like to thank Docker’s security and engineering teams for investigating the report, coordinating the CVE, and working through the regressions introduced by the initial fix.

Responsible Disclosure Timeline

April 11, 2026 – We reported the vulnerability to Docker with technical details and reproduction information.

April 14, 2026 – Docker acknowledged receipt and began reviewing the report.

April 15, 2026 – Docker confirmed the behavior as a valid finding and explained its assessment of the client-side privilege boundary.

July 10, 2026 – The initial 90-day disclosure period elapsed without a public patch. We requested a concrete remediation, advisory, and publication schedule.

July 14, 2026 – Docker described two remediation tracks: extraction-library hardening as the primary security fix and source-walk hardening as follow-up work. We agreed to delay publication while the fixes were prepared.

July 24, 2026 – Docker provided CVE-2026-17106 / GHSA-hfg8-hc9c-6c3h and targeted an August 3 coordinated release.

July 27, 2026 – Docker confirmed that the source-walk TOCTOU would not receive a separate CVE and would instead be addressed as defense-in-depth hardening.

July 30, 2026 – moby/go-archive v0.3.0 and Docker Engine/CLI 29.7.0 shipped the extraction fix.

July 31, 2026 – Docker reported significant functional regressions caused by the initial hardening and requested a final extension to August 10. Follow-up go-archive and Engine releases addressed the regressions.

August 1, 2026 – We agreed to extend coordinated disclosure to August 10.

August 6, 2026 – Docker Sandboxes 0.38.0 shipped a fix for a destination-escape flaw in sbx cp copy-out under CVE-2026-17106. Docker also confirmed that the Docker Desktop update remained scheduled for August 10.

August 10, 2026 – Docker released Docker Desktop 4.86.0, bundling Docker Engine 29.7.2 and delivering the corresponding Docker Desktop fix.

Scroll to Top