The moving company that lets a stowaway reroute every box to an address of their choosing
CVE-2026-17106, dubbed CopyEscape by Imperva researcher Ron Masas, is a symlink-following path traversal in moby/go-archive — the tar extraction library embedded in Docker Engine, Docker CLI, Docker Desktop, Docker Compose, and Docker Sandboxes. The Unpack, UnpackLayer, Untar/UntarUncompressed, and ApplyLayer functions validate archive entry paths with lexical string checks only. A malicious container process exploits a TOCTOU race during docker cp: it watches via inotify for the daemon's WalkDir to begin, then atomically swaps a directory for an absolute symlink pointing outside the extraction root. The daemon emits stale entries into the tar stream; the CLI extracts them through the dangling symlink, writing arbitrary files with the CLI user's privileges. On Linux with sudo, the attacker overwrites /usr/bin/runc and achieves root code execution the next time *any* container lifecycle event fires. Affected versions: moby/go-archive < 0.3.0, Docker Engine/CLI < 29.7.0, Docker Desktop < 4.86.0, Docker Compose < 5.4.0, Docker Sandboxes < 0.38.0.
The GHSA advisory (GHSA-hfg8-hc9c-6c3h) assigns CVSS v4.0 7.1 (HIGH). That score is directionally correct: the local attack vector (AV:L), active user-interaction requirement (UI:A — someone must invoke docker cp against the malicious container), and TOCTOU prerequisite (AT:P) keep this out of CRITICAL territory for *most* deployments. However, the advisory underweights the blast radius in CI/CD environments where docker cp is scripted to extract build artifacts — there the 'user interaction' is an automated pipeline step, not a human exercising judgment. Imperva's public PoC demonstrates reliable exploitation on Linux (runc overwrite → root code execution) and macOS (LaunchAgent persistence), converting a bare file-write primitive into full host compromise. The gap between the 7.1 advisory score and real-world CI/CD impact warrants a noisgate assessment of 7.5 HIGH.
5 steps from start to impact.
Attacker prepares malicious container
LD_PRELOAD interposer and an inotify-based watcher script. The interposer presents a deceptive filesystem view: a target path appears as a regular directory to the daemon's WalkDir. A large sentinel file provides a reliable timing signal — when the daemon accesses it, the watcher knows the walk is mid-flight. This setup is entirely within default Docker container capabilities; no privileged flags are needed.- Attacker controls or influences a container image (public registry push, malicious PR modifying Dockerfile, compromised private registry, or lateral movement into a running container)
- Container runtime allows inotify and LD_PRELOAD (default Docker configuration)
- Attacker must get their image onto a target host or into a CI pipeline — this requires either social engineering (malicious PR), a supply-chain compromise, or prior access to a container registry
proc.env contains LD_PRELOAD).Victim invokes docker cp from the malicious container
docker cp <container>:/path /host/destination. This triggers the Docker daemon to walk the container's filesystem and serialize it into a tar stream sent to the CLI for extraction. The container must be running (not stopped) for the TOCTOU race to succeed — the malicious process needs to execute the directory-to-symlink swap during the walk. In CI/CD pipelines, build scripts commonly use docker cp to extract compiled artifacts, test reports, or coverage files.- Someone runs
docker cpFROM the malicious container (copy-out direction, not copy-in) - Container is in running state at the time of the cp invocation
- Requires explicit user action or a CI script that uses
docker cp— many production Docker workflows never invoke this command - Kubernetes environments use
kubectl cpwhich routes through the exec API and a different tar implementation, not moby/go-archive - Stopping the container before
docker cpcompletely neutralizes the attack
dockerd --log-level=info) record docker cp operations including source container ID. EDR agents on the host can monitor for docker process invocations with cp subcommand.TOCTOU race: directory swapped for absolute symlink
filepath.WalkDir traverses the sentinel directory, the container's inotify watcher fires. It atomically replaces the directory with an absolute symlink pointing outside the extraction root — e.g., to /usr/bin/ on Linux or ~/Library/LaunchAgents/ on macOS. The daemon has already recorded the path as a directory and continues emitting child entries as if walking under the original tree. Imperva's PoC demonstrates this is a deterministic, repeatable exploit, not a one-shot gamble — the large sentinel file provides a wide, reliable timing window.- Container process has inotify access and can create symlinks (default capabilities)
- Race window is held open by the sentinel file technique
- The TOCTOU window exists inherently in the WalkDir→tar pipeline; the sentinel file technique effectively eliminates timing uncertainty
container.id != host AND evt.type = symlinkat AND fd.name contains /..) could catch the symlink creation. The swap from directory to symlink on the same path is an anomalous filesystem event.CLI extracts payload through symlink to arbitrary host path
hdr.Linkname from the tar header without re-validating. Subsequent entries extracted after the symlink are written through it, landing wherever the OS resolves the link — outside the intended destination directory. Files are created with the permissions of the user running docker cp.- Docker CLI is unpatched (< 29.7.0)
- The host filesystem path targeted by the symlink is writable by the CLI user
- Without sudo/root, writes are limited to the CLI user's writable paths — shell profiles, SSH authorized_keys, source code — dangerous but not system-level
- macOS Gatekeeper and TCC prompts may block writes to some protected locations
~/.ssh/, ~/.bashrc, ~/Library/LaunchAgents/ catch the macOS persistence path.Privilege escalation via runc binary overwrite
docker cp runs as root (common with sudo docker cp or in CI workers running as root), the attacker symlinks to /usr/bin/ and overwrites the runc binary with a malicious payload. The next container lifecycle operation — docker run, docker exec, docker stop — invokes the compromised runc as root. Imperva's PoC confirms this path: the replaced binary creates a root-owned marker file, proving arbitrary code execution as PID 1's parent. This converts a file-write primitive into persistent root-level host compromise.docker cpruns as root or with sudo- Writable access to /usr/bin/runc (standard for root)
- Any subsequent container lifecycle event triggers the payload
- Rootless Docker configurations are immune — runc is not writable
- Organizations restricting sudo for Docker operations block this escalation path
- Docker Desktop for macOS runs the daemon in a Linux VM; runc overwrite compromises the VM, not the macOS host directly
/usr/bin/runc — a system binary change outside of a package manager transaction is a high-fidelity alert. Binary integrity checking against known-good hashes catches this immediately.WalkDir. Running docker stop <container> before docker cp completely breaks the race — no process can manipulate the filesystem mid-walk. This is the fastest, most effective compensating control. Deploy within 30 days per the noisgate mitigation SLA for HIGH severity. Update all CI/CD pipeline scripts, runbooks, and developer documentation to enforce docker stop before any docker cp operation.docker cp to extract artifacts, mount the output directory as a bind mount (docker run -v /host/path:/container/output ...) or use a named volume (docker volume create artifacts && docker run -v artifacts:/output ...). This eliminates the tar serialization/extraction pipeline entirely and removes the vulnerable code path from the workflow. In CI/CD, refactor build scripts to use --mount type=bind for artifact extraction. Deploy within 30 days per the noisgate mitigation SLA for HIGH severity./usr/bin/runc). File writes are limited to the CLI user's writable paths — shell profiles, SSH keys, source code — still dangerous but the host-takeover-via-runc path is eliminated. Configure rootless Docker (dockerd-rootless-setuptool.sh install) or restrict sudo access for Docker operations. In CI/CD, run build steps as a non-root user where possible. Deploy within 30 days.docker cp) last. For Go projects importing moby/go-archive directly, run go get github.com/moby/[email protected] and rebuild.- Container image scanning (Trivy, Grype, Snyk Container): The exploit payload is runtime behavior — an LD_PRELOAD interposer and inotify watcher that execute when the container runs, not a vulnerable package in the image manifest. Static image scanning will never flag this.
- SELinux / AppArmor container profiles: These policies restrict what the container process can do, but the directory-to-symlink swap and inotify operations are permitted under default Docker SELinux and AppArmor profiles. More importantly, the actual file writes happen on the host side during CLI extraction, entirely outside the container's security context.
- Network segmentation / firewall rules: CVE-2026-17106 is a local attack —
docker cpis a local CLI operation communicating with the daemon over a Unix socket. Network controls have zero bearing on exploitability. - Docker Content Trust (DCT) / image signing: DCT verifies image provenance and integrity, not runtime behavior. A validly signed image from a trusted publisher could still contain the LD_PRELOAD exploit payload if the publisher's build pipeline is compromised or if a developer intentionally adds it.
- Read-only container filesystem (
--read-only): The exploit uses inotify and symlink creation in the container's writable layers. The--read-onlyflag only affects the root filesystem; tmpfs mounts (which are writable by default) and any volume mounts still allow the symlink swap. Additionally, the exploit needs the target path to exist in the container's filesystem view, which it does in the default overlay.
The supporting signals.
| In-the-Wild Status | No confirmed in-the-wild exploitation. Not listed in CISA KEV. No GreyNoise tags or Shadowserver scan activity reported as of 2026-10-07. The local attack vector (docker cp must be explicitly invoked) makes opportunistic mass exploitation unlikely — this is a targeted attack primitive, most dangerous in CI/CD and supply-chain scenarios. |
|---|---|
| Proof of Concept | Public, reliable, weaponized PoC available. Imperva published a detailed technical write-up (blog) and the exploit is mirrored at HackSpeak/CVE-2026-17106 (MIT license). Includes macOS variant (non-destructive shell profile overwrite) and Linux variant (runc overwrite → root code execution). Also indexed in XZ1r0/cve-2026-poc-collection and SecureWithUmer/CVE-2026-PoCs. The PoC uses inotify + sentinel file to make the TOCTOU race deterministic. |
| EPSS | 0.0044 (0.44%) — low predicted probability of exploitation in the next 30 days (~25th percentile). Consistent with the local attack vector and user-interaction requirement. EPSS does not account for CI/CD automation scenarios where the 'user interaction' is a script. |
| KEV Status | Not listed in the CISA Known Exploited Vulnerabilities catalog as of 2026-10-07. |
| CVSS Vector | CVSS v4.0: 7.1 CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N — Local, low complexity, attack prerequisites (TOCTOU timing), no privileges, active user interaction, high C/I/A impact on vulnerable system, no cross-system impact. CVSS v3.1: 7.8 CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H. Both sourced from GHSA-hfg8-hc9c-6c3h. |
| Affected Versions | moby/go-archive < 0.3.0 · Docker Engine/CLI < 29.7.0 · Docker Desktop < 4.86.0 · Docker Compose < 5.4.0 · Docker Sandboxes < 0.38.0. Any Go binary importing github.com/moby/go-archive at a version below v0.3.0 and extracting untrusted archives is also vulnerable. |
| Fixed Versions | moby/go-archive v0.3.0 (0.3.0" target="_blank" rel="noopener">release) · Docker Engine/CLI 29.7.0 (29.7.2 recommended) · Docker Desktop 4.86.0 · Docker Compose 5.4.0 · Docker Sandboxes 0.38.0. Red Hat tracks backports via RHSA; Debian/Ubuntu via docker-ce packages. Go module consumers: go get github.com/moby/go-archive@v0.3.0. |
| Exposure Surface | Docker Engine is not typically internet-exposed (daemon socket is local or TLS-secured). The attack vector is local (docker cp), so Shodan/Censys exposure data is not directly applicable. Primary risk surface: CI/CD workers (Jenkins, GitLab Runner, GitHub Actions self-hosted runners, CircleCI) that script docker cp to extract build artifacts, and developer workstations using Docker Desktop. |
| Disclosure Timeline | 2026-08-18: Advisory published, CVE assigned, patches released (Docker Engine 29.7.0, Desktop 4.86.0). 2026-08-20: Advisory updated. Fix PR: moby/go-archive#45, vendored into Docker Engine via moby/moby#53247. |
| Reporting Researcher | Ron Masas, Imperva Red Team. Remediation by thaJeztah (Sebastiaan van Stijn, Docker/Moby maintainer). Reviewed by vvoland and mickael-docker. Vulnerability dubbed CopyEscape. |
Sources.
- Imperva Blog — CopyEscape Technical Write-up (Ron Masas)
- GitHub Security Advisory GHSA-hfg8-hc9c-6c3h
- moby/go-archive v0.3.0 Release Notes
- HackSpeak/CVE-2026-17106 — Public PoC Repository
- GitLab Advisory Database — CVE-2026-17106
- SecureLayer7 Labs — CVE-2026-17106 Analysis
- NHS England Cyber Alert CC-4828 — Docker CopyEscape
- Docker Engine PR #53247 — Vendor go-archive v0.3.0
Why this verdict
- Reliable public PoC with host-takeover path: Imperva's CopyEscape PoC demonstrates deterministic exploitation — the inotify + sentinel-file technique converts the TOCTOU race from probabilistic to repeatable. On Linux with sudo, the runc overwrite delivers immediate root code execution with persistence across all future container operations. This is not a theoretical crash-only demonstration; it is a weaponized exploit with two platform-specific payloads.
- CI/CD supply-chain vector elevates real-world impact: In CI/CD pipelines,
docker cpis commonly scripted to extract build artifacts, test reports, and coverage data from builder containers. An attacker who contributes a malicious Dockerfile via a pull request triggers the exploit automatically when the CI worker executes the build-and-extract flow. The CVSSUI:A(active user interaction) is a *machine*, not a human making a judgment call. This is supply-chain-scale impact — a compromised CI worker can poison downstream artifacts, inject backdoors into release binaries, or pivot laterally into the build infrastructure. - Role multiplier: Docker Engine is a canonically high-value component (container orchestration per the role catalog). On CI/CD workers (~20–30% of Docker Engine deployments by industry surveys), the chain succeeds with supply-chain-scale blast radius — the
docker cpinvocation is a standard build-script step, and the CI worker typically runs as root. On production container hosts,docker cpis rarely used in normal operations; Kubernetes clusters use containerd/CRI-O withkubectl cpvia the exec API, not moby/go-archive. On developer workstations (~40–50% of Docker Desktop installs), blast radius is host-level (shell profiles, SSH keys, source code). The CI/CD case floors this at HIGH; the production-host case does not escalate further becausedocker cpis not part of the standard container lifecycle. - User interaction requirement prevents CRITICAL: The CVSS v4.0 vector includes
UI:A—docker cpmust be explicitly invoked against the malicious container. Outside of CI/CD, this means the attacker must social-engineer or trick a human into running a specific command against their container. The container must also be in a running state. These two prerequisites narrow the exploitable population below the CRITICAL threshold for the general Docker install base. - Local attack vector, no remote chain:
AV:Lmeans there is no network-reachable path to triggering this vulnerability. The attacker needs either a malicious image on a registry that someone pulls and runsdocker cpagainst, or existing code execution inside a container. Combined with the user-interaction gate, this bounds the realistic attack surface.
Why not higher?
CRITICAL would require either a remote attack vector or exploitation without active user interaction. CopyEscape has neither: the attack is local (AV:L), requires explicit invocation of docker cp against the malicious container (UI:A), and the container must be running. While Docker Engine is canonically high-value, docker cp is not part of the default container lifecycle — it is an operator-initiated or script-initiated action. In production Kubernetes environments (the largest segment of enterprise container infrastructure), docker cp is not used: kubectl cp routes through a completely separate code path. The fraction of Docker hosts where docker cp is a routine automated operation (primarily CI/CD) is substantial but not universal enough to warrant CRITICAL across the install base.
Why not lower?
MEDIUM would dangerously underweight the blast radius. The public PoC is reliable and weaponized, not crash-only or theoretical. On Linux with sudo, runc overwrite gives immediate root code execution on the host with persistence — every future container lifecycle event re-triggers the payload. In CI/CD environments, this is a supply-chain attack with zero human judgment in the loop, targeting infrastructure that builds and signs release artifacts. The library (moby/go-archive) is embedded in every Docker installation worldwide — the affected population is enormous. An EPSS of 0.44% reflects the local/UI-gated attack vector, not the severity of successful exploitation.
Crowdsourced verification payload.
Run on each Docker host (Linux or macOS) as any user with permission to invoke docker version. No sudo required. Example: bash check_cve_2026_17106.sh. For Go projects importing moby/go-archive, the script also checks go.sum if present in the current directory.
#!/usr/bin/env bash
# CVE-2026-17106 (CopyEscape) — Docker & moby/go-archive version checker
# Run on Docker hosts. No special privileges required.
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -uo pipefail
PATCHED_ENGINE="29.7.0"
PATCHED_GOARCHIVE="0.3.0"
STATUS="UNKNOWN"
version_gte() {
# Returns 0 if $1 >= $2 using version-aware sort
printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1 | grep -qx "$2"
}
echo "=== CVE-2026-17106 (CopyEscape) Check ==="
echo ""
# --- Check Docker Engine/CLI ---
if command -v docker &>/dev/null; then
CLI_VER=$(docker version --format '{{.Client.Version}}' 2>/dev/null || true)
if [ -z "$CLI_VER" ]; then
CLI_VER=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
fi
if [ -n "$CLI_VER" ]; then
echo "[*] Docker CLI version: $CLI_VER"
if version_gte "$CLI_VER" "$PATCHED_ENGINE"; then
echo "[+] Docker CLI >= $PATCHED_ENGINE — PATCHED"
STATUS="PATCHED"
else
echo "[-] Docker CLI < $PATCHED_ENGINE — VULNERABLE"
STATUS="VULNERABLE"
fi
else
echo "[?] Docker found but CLI version undetermined"
fi
SRV_VER=$(docker version --format '{{.Server.Version}}' 2>/dev/null || true)
if [ -n "$SRV_VER" ]; then
echo "[*] Docker Engine version: $SRV_VER"
if version_gte "$SRV_VER" "$PATCHED_ENGINE"; then
echo "[+] Docker Engine >= $PATCHED_ENGINE — PATCHED"
else
echo "[-] Docker Engine < $PATCHED_ENGINE — VULNERABLE"
STATUS="VULNERABLE"
fi
fi
else
echo "[?] docker CLI not found on this host"
fi
# --- Check Go module dependency (if in a Go project) ---
if [ -f go.sum ]; then
GA_VER=$(grep 'github.com/moby/go-archive v' go.sum 2>/dev/null | head -1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || true)
if [ -n "$GA_VER" ]; then
GA_CLEAN=${GA_VER#v}
echo "[*] moby/go-archive in go.sum: $GA_VER"
if version_gte "$GA_CLEAN" "$PATCHED_GOARCHIVE"; then
echo "[+] moby/go-archive >= v$PATCHED_GOARCHIVE — PATCHED"
[ "$STATUS" != "VULNERABLE" ] && STATUS="PATCHED"
else
echo "[-] moby/go-archive < v$PATCHED_GOARCHIVE — VULNERABLE"
STATUS="VULNERABLE"
fi
fi
fi
echo ""
echo "=== RESULT: $STATUS ==="
case "$STATUS" in
PATCHED) exit 0 ;;
VULNERABLE) echo "Action: update Docker to >= $PATCHED_ENGINE or moby/go-archive to >= v$PATCHED_GOARCHIVE"
echo "Interim: stop containers before docker cp, or use bind mounts instead"
exit 1 ;;
*) echo "Could not determine patch status — verify manually"
exit 2 ;;
esac