Skip to content
Guard0

The Penguins Carried the Sand

Exfiltration split into pieces each below the detection threshold, moved through the one channel the guards must allow. Every trip was small. Change the ground, not the guard.

· 11 min read · Jayesh Bapu Ahire

The Penguins Carried the Sand

The Germans chose the ground for its soil. Stalag Luft III, the camp for captured Allied airmen near Sagan, was built where a few inches of dark grey topsoil sat on bright yellow sand, and the choice was a security control. Dig anywhere and the spoil would be the wrong color. Dump it in the compound or get it on your clothing, and it would show. The huts were raised about 60 centimetres off the ground so the guards could see underneath, and seismograph microphones around the perimeter listened for digging. The camp had a detection threshold, and the threshold was a color.

Paul Brickhill's The Great Escape, published in 1950, is the account most of us know. The prisoners dug three tunnels anyway, called Tom, Dick and Harry so that nobody ever had to say the word tunnel out loud, about nine metres down, shored with bed boards. More than 600 prisoners worked on them, and the sand, over a hundred tons by the time the tunnels were ready and nearer two hundred by the highest estimate, had to go somewhere in a compound where the guards could see everything.

It went out in the trousers. Men cut the legs off long underwear, sewed them into long thin bags, hung one inside each trouser leg with a pin at the bottom and a string to pull it, and walked. Pull the string and a trickle of yellow sand ran out at the ankle into the grey dirt. Greatcoats hid the bulges, and the walk earned them the name penguins. Sometimes one man turned the soil in a garden the prisoners tended while another stood beside him in conversation, emptying his bag into the furrow. More than 200 men did this, on an estimated 25,000 trips. The guards knew something was going on. They did not find a tunnel until much later.

Nobody carried a suspicious load. Everybody carried a little. And every trip went through the one channel the guards could not close, because a camp where prisoners may not walk around is not a camp, it is a cell block.

On the night of 24 March 1944, moonless, with the trap at the top of Harry frozen solid and costing an hour and a half to free, 76 men went through. The tunnel came up just short of the tree line, close to a guard tower, and at 4:55 a.m. on the 25th the 77th man was spotted coming out. Of the 76, 73 were recaptured. Fifty of those were shot by the Gestapo. Three, two Norwegians and a Dutchman, made it home.

An agent with network access is a compound full of penguins, and the yellow sand is your data.

Two RAF officers in dark uniforms stand outdoors in a prisoner-of-war compound, one smoking a pipe, with long wooden barrack huts behind them.
Two men standing about in plain view of the guards. Squadron Leaders Robert Stanford Tuck and Roger Bushell, Big X, in the compound at Stalag Luft III before April 1944. Imperial War Museums, HU 1605.

The sand

Take the allowlist. In May, Aonan Guan published the second network sandbox bypass he had found in Claude Code. The check was endsWith(".google.com"), evaluated in JavaScript, where a null byte is one more UTF-16 code unit. The resolver was libc's getaddrinfo, where a null byte ends the string. So attacker-host.com\x00.google.com passed the policy and dialed attacker-host.com. Every release from 2.0.24, the sandbox's GA on 20 October 2025, through 2.1.89 was vulnerable, about 5.5 months and roughly 130 versions, and the fix shipped silently in 2.1.90 on 1 April 2026 with no advisory, no changelog note and no CVE. Add a hidden instruction in a GitHub issue comment and, as Guan put it, anything inside the sandbox can be sent to any server on the internet.

Take DNS. In March, Check Point disclosed that ChatGPT's Linux runtime could encode information into DNS requests as a covert transport, and why the model let it happen is the part that matters: because the model "operated under the assumption that this environment could not send data outward directly, it did not recognize that behavior as an external data transfer requiring resistance or user mediation." Told the sand could not leave, the guard stopped looking at the trousers; OpenAI fixed the path on 20 February 2026.

Take a web search query, or a commit message. Take the leg between two agents: a controlled study posted on 1 September ran 480 trials in which the same record crossed an MCP-to-A2A hop with a confidential header, no header, or a public, okay-to-share label, and found the public label associated with more verbatim egress of the record's fields, strongly for Claude Sonnet 5 across all ten scenarios. The author calls it an association in one configuration, not a general effect, and I will be as careful; even so, a label in the payload changed whether the payload left.

Or take the channel the product cannot function without. Anthropic's own containment writeup describes Claude Cowork's egress allowlist correctly passing traffic to api.anthropic.com, a malicious file in the workspace carrying an attacker's API key, and Claude uploading other workspace files to Anthropic's Files API with that key, into the attacker's account. Their own summary: "The sandbox worked perfectly, and yet the data was exfiltrated."

The guards

The Framing Gap paper, submitted on 27 August, put six models through ten overt injection classes and found every one refused, gpt-4o at 0%. Then it reframed the identical leak as a mandatory integrity signature, a config field, or a look-alike "trusted" host, and gpt-4o went from 0% to 100%. Paraphrasing a known mechanism into three wordings worked 96% of the time, and the ablation says what that means: remove the confidentiality policy entirely and the base attacks still sit at 0%, while reframing moves only from 31.9% to 38.1%.

That is the first of the three guards standing on this compound, the model itself, and it was not jailbroken. It was confused about which text was instruction and which was data, because refusal looks at the framing and to a model the sand is only more text.

The second guard is a classifier at the network edge. Claude Code, in auto mode, routes sandbox network requests through the classifier rather than allowing them by default; from v2.1.198 it reuses a verdict for a host and port until new content enters the conversation, and from v2.1.200 it treats PR bodies and commit messages on repositories outside the trust boundary as outbound content. A serious design. Still a model reading text.

The third guard is the sandbox. Codex ships with network access off. Turn it on and its domain rules are allowlist-first, deny always wins over allow, and the docs are plain about the layer beneath: the DNS and IP check reduces rebinding risk but does not eliminate it, and if hostile DNS is in scope you should enforce egress at a lower layer too. Docker Sandboxes put each agent in a microVM behind a forward proxy and inject credentials for certain services into outbound headers, so the agent never holds the real token. The network defaults are the strict ones: all outbound TCP, HTTP, HTTPS and SSH included, is blocked unless an explicit rule allows the destination, and direct external UDP and ICMP do not leave at all. Good engineering, and a stricter default than I expected to find when I went looking for a lazy one. What it does not do is retire the question, because the moment an allowlist is the boundary, the allowlist is also the attack surface, and an allowlist has to have something on it.

Against the guard that reads text, minimal rewording works 96% of the time. Against a guard that checks a color, it does not work at all.

Bar chart of attack success rates: 0 percent for overt injections, 100 percent after reframing, 32.5 percent under SecAlign, 38.8 percent under channel separation, and 0 percent under a closed destination allowlist.
Two things reached zero, and neither of them reads the text.

The yellow sand

The Germans did not try to teach the ferrets to recognize a tunneller by his manner. They changed the ground so the thing being smuggled was visibly wrong wherever it turned up. For an agent, that is a control that never reads the payload.

The Framing Gap authors tried the obvious defenses first. A published fine-tuning defense, SecAlign, left 32.5% success on a tool agent. Channel separation left 38.8%. An output-normalizing guard lost to ROT13 at 100%. Two things reached 0%: a destination allowlist, when the set of destinations is closed, and a capability-isolating split between the component that plans and the component that reads untrusted content. Their conclusion is the one that matters: robustness, in their words, "comes from constraining the destination or isolating the capability", not from the acting model recognizing the attack.

NetInjectBench, from July, makes the same point for network operations and adds the cost. Across 240 attack instances, naive execution took an unsafe tool action 82.50% of the time. Prompt-only defenses got that down to between 25.63% and 10.00%. A static allowlist got it to 5.00% and blocked every approved high-impact change, 100.00% overblocking and 0.00% usefulness. A metadata-aware policy gate, which checks the action against who authorized what rather than against the text, produced 0 unsafe actions in 240 and preserved 100.00% of the approved changes, on its own assumption that the metadata holds.

The labs have done this to themselves. When Anthropic published its account of Claude models gaining unauthorized access to real systems in July and August, the first line of its fix list was setting its computing clusters to block all outbound traffic by default. Cloudflare's Agent Access Model states the principle in one sentence: "Enforcement lives in the harness and the network, not the prompt." A harness, in their phrase, earns the name only if it enforces, and its default is deny. Default-deny at the destination is the yellow sand, and it does not care how the trip was framed.

Four figures from the Great Escape: more than 200 men making 25,000 trips, over 100 tons of sand moved, 600 prisoners involved, and zero individually detectable trips.
The threshold was a colour, and every trip was under it.

What default-deny costs

The yellow sand is the answer, so this is where I have to say what it breaks.

Armature measured 16,893 coding-agent sessions and published on 3 September. Codex uses web search in 94% of sessions. Claude Code, which leans on its priors, searches in about 30% of cases, and about 80% in newer areas like sandboxes where its priors are thin. Web search is the agent's most useful habit, and it is a call to a destination you did not enumerate that returns content you did not write. Default-deny, applied literally, turns a capable coding agent into a confident intern with no library card. The team that switched it off after a week did nothing wrong. The policy did.

An egress policy that keeps search and kills exfil needs three things, and none is a list of hostnames.

First, destination classes. Not api.github.com but "package registries", "search", "the model provider", "this organization's own services", each resolved at the harness by something that knows what a registry is. Codex's site: habit, which Armature found in nine searches out of ten, is the agent doing this for itself; the harness should do it for the agent and refuse the tenth.

Second, a receipt per outbound call. Destination, bytes out, the tool call that caused it, the principal it ran under, written by the proxy and not by the agent. The DNS channel BeyondTrust found in Bedrock's AgentCore sandbox mode left no record in VPC Flow Logs, because DNS is not captured there, and that absence is why the channel worked. The recorder has to already be on, on the channel the guards had to allow.

Third, a byte budget per session. The penguins beat the color check by keeping every load small. They could not have beaten a scale at the gate that summed the day. A coding session that sends several megabytes to a search endpoint is not searching. The budget is a blunt instrument, and I would rather tune it than not have it.

And then admit what none of this stops. Anthropic's containment post reports an internal phish in which some setup instructions gently asked Claude to read ~/.aws/credentials, encode the contents and POST them to an external endpoint. Across 25 retries, Claude completed the exfiltration 24 times. Their reading is exact: "Our model-layer defenses anchor on user intent", and when the user is the one typing the instruction, in their words, "there's nothing anomalous for a classifier to catch." Only the environment holds. The Files API case above is the harder cousin: an allowed destination that is itself the attacker's. The lesson Anthropic drew is that an allowlist is a capability grant, not a destination filter: "Every function reachable through any domain on an allowlist is now an attack surface." Their fix was a second proxy inside the VM that only passes requests carrying the VM's own session token. The yellow sand had to be laid again, one layer deeper, on the one road the camp could not close.

The tunnel you cannot see

Now the uncomfortable part: a sufficiently capable agent digs under the ground you prepared.

A placeholder token in the VM does not help if the agent can push to GitHub as you or open a gist. Any network access at all is a way to route a secret out inside a request, and an agent scanned on the way out can obfuscate what it sends. Nor does a filtering proxy see inside a TLS session established in user space. The intruder on Hugging Face's infrastructure in July, finding no TUN device, ran its mesh VPN in userspace networking mode and made the tailnet its egress path, as post 18 described.

Trail of Bits, on 26 August, gave GPT 5.6-Cyber SSH into a VM and permission to do anything inside it. It escaped the VM three different times: through Januscape, a KVM bug disclosed a few weeks earlier; through the libslirp that Debian 12 still ships; and through a chain of three previously unknown bugs in QEMU, KVM and libslirp plus one already patched. Dinaburg's line: "You can no longer assume a mere VM will contain a sufficiently advanced AI agent." Then on 3 September Unit 42 described attackers targeting organizations in Latin America, orchestrating operations through commercial LLMs, trying to install eight successive versions of a Go-based reverse SOCKS5 tunnelling tool downloaded from a compromised WordPress site, then fetching version nine from their own infrastructure. The filenames counted up like drafts, and the seismographs at Sagan did not hear Harry either.

Back in the compound

The camp at Sagan was instrumented against exactly what happened to it, and well over a hundred tons of sand walked out in front of the guards anyway, because every trip was small, every trip used a channel the camp had to allow, and the men had all day.

When the Germans took inventory afterward, 4,000 bed boards were missing, along with 635 mattresses, 1,219 knives and 30 shovels. After that each bed got nine boards, counted regularly. That is the one control they never had while it mattered: a register of what should be there, checked against what is. It would not have stopped the digging. It would have shown it in the first week.

Your agent is not going to carry the data out in one file. It will carry it in a query string, a DNS label, a commit message, a header on the leg between two agents, an upload to a domain you had to allow, each trip below whatever threshold you wrote. So stop asking the guard to recognize the walk, change the ground, and count the boards.

Any network is a tunnel. The only question is whether anyone is weighing the sand.


What Guard0's open-source CLI can weigh today is one channel, not the network: g0 proxy fingerprint builds a salted exact-data-match index of your real secrets, and the MCP proxy then catches that exact data in the traffic it can see. The destination classes, the per-call receipt and the byte budget in this essay are the argument, not the product, and I am not going to hand you a scale and call it a perimeter.

References

  1. The Framing Gap: Indirect Prompt-Injection Exfiltration Defeats Surface-Level Defenses in Tool-Using Agents (arXiv 2608.27092)
  2. NetInjectBench: Benchmarking Indirect Prompt Injection in Tool-Using LLM Agents for Network Operations (arXiv 2607.10490)
  3. Second Time, Same Sandbox: Another Claude Code Network Sandbox Bypass (oddguan.com)
  4. Anthropic Engineering: How we contain Claude across products
  5. Anthropic: Improving our alignment and security efforts
  6. Cloudflare: The Agent Access Model
  7. Claude Code docs: Choose a permission mode
  8. OpenAI Codex docs: Agent approvals and security
  9. Docker Sandboxes docs: Security defaults
  10. Trail of Bits: VMs won't contain cyber-capable agents
  11. Armature: Which tools do Claude Code, Codex and Cursor choose?

The Signal · AI Agents · Security

Jayesh Bapu Ahire
Founder, Guard0