docker cp copy-out can write outside the destination (strings.HasPrefix path containment + walk/extract symlink race)
Summary
docker cp copy-out (container → host) can write a container-controlled file outside the requested destination via two stacked defects: a raw-string prefix check on the extraction side and a walk/extract race window in tar stream production. Validated with a proof-of-concept on Docker Engine 29.6.0 (API 1.55).
Root cause 1 — weak path containment on extraction
In vendor/github.com/moby/go-archive/archive.go (extraction path):
targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname)
if !strings.HasPrefix(targetPath, extractDir) {
// reject
}strings.HasPrefix compares raw strings, not path boundaries:
extractDir = /tmp/dst, symlink target= /tmp/dst2strings.HasPrefix("/tmp/dst2", "/tmp/dst")→true(passes)- but
dst2is a sibling ofdst, outside the requested destination
A path-boundary check avoids this class:
rel, err := filepath.Rel(extractDir, targetPath)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
// reject
}Root cause 2 — race window between walk and extraction
In daemon/archive_unix.go (archive creation) and vendor/github.com/moby/go-archive/archive.go:
- Tarballer walks the source tree with
filepath.WalkDir. - Walk observes a directory entry.
- Race: a process inside the container replaces that directory with a symlink pointing at a host-reachable target.
- Tar entries below that logical path are then written through the symlink on extraction.
The daemon already documents a time-of-check/time-of-use caveat around archive paths (daemon/container/container.go), but the copy-out extraction path does not close this window.
Impact
Arbitrary file write to a host path outside the requested docker cp destination, with content controlled by the container. Preconditions:
- Attacker controls files/processes inside a running container.
- Host user runs
docker cpfrom that container to a destination that has a sibling path sharing the destination name as a raw string prefix (root cause 1), or the race wins during tar production (root cause 2).
This matters in any workflow where containers are less trusted than the host destination (shared CI runners, multi-tenant hosts, copied artifacts feeding privileged pipelines).
PoC validation
Tested on Docker Client/Server 29.6.0, API 1.55. The PoC creates 12k padding files, then races a mkdir → ln -s ../../../dst2 swap against docker cp, sweeping delays 0.010s–5.500s:
delay=0.200 cp_status=0 outside_marker=present link=../../../dst2
success=yes
requested_destination=.../dst
outside_marker_path=.../dst2/marker
outside_marker_value=container-controlled-host-marker
observed_symlink=.../dst/src/dir/zzlink -> ../../../dst2Note cp_status=0 — the copy succeeds while writing outside the destination.
Suggested fix direction
The filepath.Rel check closes root cause 1 but not the symlink race. A robust design extracts relative to an opened directory file descriptor (openat-style, O_NOFOLLOW per component) so a mid-stream swap cannot redirect writes. Fallback mitigation for users: copy only from stopped containers, or from an immutable snapshot.
Environment
- Reproduced: Docker 29.6.0 (Linux/amd64).
- Also affects: Docker 29.8.1 (Arch Linux).
- Source PoC and validation logs:
agnusdei1207/exploitarium, directorydocker-cp-copyout-destination-escape/.
Filed from automated security analysis, 2026-09-27.
Source: moby/moby