# Docker's agent sandbox leaked through its copy command. Check every path that crosses the wall.

> A symlink let an agent in Docker Sandboxes reach host files through sbx cp. Why every file channel needs its own boundary, with a sketch of one.

URL: https://www.voirlabs.io/insights/docker-sandbox-copy-symlink-escape
Author: Ijlal Ishaq, AI systems & backend, Voir Labs (https://www.voirlabs.io/team/ijlal)
Published: 2026-11-04
Tags: AI agents, Security, Developer tools

A sandbox for an AI agent is only as strong as the least guarded way in or out of it. Docker Sandboxes put a coding agent in its own virtual machine and let it see only the folders you approve, and that part held. The gap was the copy command next to it. By the end of this article you will know why that keeps happening, and what a file channel that respects the boundary looks like in code.

## The allowlist held. The copy command did not ask it.

Docker Sandboxes runs agents such as Claude Code, Codex and Gemini CLI inside a microVM with its own kernel, filesystem and network. You grant it specific folders, usually the project workspace, and those folders are shared into the VM. Together they form an allowlist: the only parts of your machine the agent is supposed to reach.

On November 3, Peyton Kennedy of Endor Labs published CVE-2026-103916, a flaw he found in `sbx cp`, the command that moves files between the sandbox and your machine. The copy runs on your side, with your permissions, and never consults the allowlist. In Kennedy's words, the CLI "resolves symlinks in the paths it is given and acts on the resolution."

The agent controls those links, because the workspace is granted read and write. So it can replace a folder in the workspace with a link to somewhere else on your machine. Inside the VM the link points nowhere, so planting it gains the agent nothing on its own. It takes effect when you run an ordinary copy that passes through that folder. Kennedy shows both directions: a copy out of the sandbox overwrites a file outside the workspace, and a copy in pulls a host file into the sandbox.

The command did have a guard, the `-L` flag, but it checks only the last part of the path. A symlink can sit in any of four positions, and in the researcher's table "Three of the four positions are open." Endor Labs rates it high, 8.2 on CVSS 3.1.

## The fix took two tries

Endor Labs reports that Docker first planned the fix for version 0.48.0 on October 13, and that "after the initial patch was live, the Docker team determined the fix was incomplete." Docker's own release notes for 0.51.0, dated November 3, confirm the final change: "`sbx cp` blocks symbolic links in mounted directories from escaping onto other host paths." If you run Docker Sandboxes, upgrade to 0.51.0.

This is also the second `sbx cp` escape this year. An earlier one, CVE-2026-17106, was fixed in 0.38.0, and Kennedy notes that its containment still works. It checked paths inside the archive being copied, not where the copy itself started or landed.

## Why the second path gets missed

Kennedy's own summary is the part worth keeping: "the isolation primitive worked, and the failure was a second path to the host that the security model did not account for." He found the same shape earlier this year in another agent sandbox, brig.

The reason is ordinary. A sandbox is designed around its main channel, the shared folder, and that channel gets the allowlist and the tests. Side channels that move files on the operator's behalf, such as copy, export, download and artifact upload, look like the operator's own action, so they inherit the operator's trust. When the agent controls the path, that trust is spent wherever the agent points it.

Anything that lets an agent produce files on a real machine has these side channels: a build step that collects outputs, a tool that saves an attachment, a job that syncs a report to shared storage. Each one needs the boundary checked again, at the moment it touches the disk.

## A copy that stays inside the workspace

Here is a sketch of how we would write the host side of a copy out of an agent's workspace in Node. It is ours, not Docker's code.

```ts
import { constants, promises as fs } from "node:fs";
import path from "node:path";

// Write bytes to a path inside the workspace, or refuse.
export async function writeInside(
  workspace: string,
  relPath: string,
  data: Uint8Array,
): Promise<string> {
  const root = await fs.realpath(workspace);
  const target = path.resolve(root, relPath);
  // Resolve every directory on the way, not only the file name.
  const dir = await fs.realpath(path.dirname(target));
  if (dir !== root && !dir.startsWith(root + path.sep)) {
    throw new Error(`refused: ${relPath} resolves to ${dir}`);
  }
  const file = path.join(dir, path.basename(target));
  // O_NOFOLLOW: if the file itself is a link, fail.
  const flags = constants.O_WRONLY | constants.O_CREAT |
    constants.O_TRUNC | constants.O_NOFOLLOW;
  const fh = await fs.open(file, flags, 0o644);
  try { await fh.writeFile(data); } finally { await fh.close(); }
  return file; // print it, so a person sees where it landed
}
```

Notice three things. The check runs on the resolved parent directory, which is exactly the position `-L` left open. The final open refuses to follow a link in the last position. And the function returns the real path, so the caller can print it; Kennedy points out that a successful `sbx cp` printed nothing, so a person watching had nothing to notice. A copy in is the same check on the source.

## Where the sketch stops

A path check and the write that follows it are two steps. An agent that can rewrite its workspace while the copy runs could swap a folder for a link in between. On Linux, `openat2` with `RESOLVE_BENEATH` makes the kernel enforce the boundary during the open itself, and Go code can use a library such as `cyphar/filepath-securejoin`, which Kennedy notes was already linked into the `sbx` binary. Use those where you can. The sketch shows the rule; the kernel enforces it.

If your agent never writes outside a VM and nothing it produces is copied back, none of this applies. The moment its output lands on a machine you use, it does.

## What to take from it

- Upgrade Docker Sandboxes to 0.51.0, and until you have, resolve a path yourself before you copy to or from a workspace.
- List every channel that moves files between your agent and a real machine, not only the shared folder.
- Check the boundary on each one after resolving every part of the path, at the moment of the write.
- Show the resolved path whenever a copy succeeds.

If you are putting agents in front of real systems and want the file paths checked before they ship, [book a conversation](https://calendly.com/voirlabs/30min).

## Sources

- Endor Labs, Peyton Kennedy, "Docker Sandboxes vulnerability lets AI agents access sensitive files outside their sandbox (CVE-2026-103916)", November 3, 2026: https://www.endorlabs.com/learn/docker-sandboxes-vulnerability-lets-ai-agents-access-sensitive-files-outside-their-sandbox-cve-2026-103916
- Docker, Docker Sandboxes release notes, version 0.51.0 (2026-11-03): https://docs.docker.com/ai/sandboxes/release-notes/

---

Voir Labs is an AI-first design and development studio (https://www.voirlabs.io/). Related service: https://www.voirlabs.io/services/ai-automation. Book a 30-minute conversation: https://calendly.com/voirlabs/30min
