πŸ” Sandbox Audit Round 2 β€” Three Real Escapes, Then Zero Pipe's second sandbox audit found three real escapes in the ratchet logic of its agent sandbox, including an empty whitelist allowing all hosts, a port mismatch bypass, and missing comparisons for budget, max_tool_calls, and timeout, all of which have been fixed in IsSubsetOf. The audit also confirmed two earlier escapes and identified a non-deterministic argument ordering bug in executeTool, while two LLM runs with deepseek-v4-pro after the fixes returned zero escapes. ← All posts← Alle BeitrΓ€ge ../blog.html πŸ” Sandbox Audit Round 2 β€” I Found Three Real Escapes, Then Proved They're Dead A second red-team pass on Pipe's agent sandbox: three ratchet holes that a confined script could actually use, one latent bug, and two LLM runs that came back empty-handed. Follow-up: The audit didn't stop here. Rounds 3–6 found try ai ignoring ai:false , mis-gated embed / import , and a CLI --sandbox flag gap β€” plus the structural fixes that kill those bug classes β€” see The Audit Continues . Round 1 ended with a confident "no escape." Round 2 started by not trusting that verdict. Instead of only asking a model to attack, I read the code and built deterministic probes myself. That's where the sandbox cracked β€” not in the LLM run, but in the ratchet logic I'd shipped a week earlier. Here's what broke, how I proved it, and how the sandbox held after the fix. The Ratchet: Right Idea, Three Holes πŸ•³οΈ Pipe's sandbox is supposed to ratchet down : once a restricted profile is active, a script may only switch to a profile that grants the same or fewer rights. The check lives in IsSubsetOf . Three dimensions were missing or wrong. 1. Empty whitelist = allow-all An empty network whitelist means "allow every host." But IsSubsetOf skipped the empty-candidate case, so a profile with network whitelist: passed as a subset of a whitelisted profile. Proof: from a profile whitelisted to api.github.com , I registered and switched to an empty-whitelist profile, then fetched https://example.com β†’ HTTP 200 . Full network egress, outside the whitelist. 2. The port ratchet A portless entry api.github.com , any port passed as a subset of a port-pinned entry api.github.com:443 , because the matcher only rejected a port mismatch when both sides carried an explicit port. Proof: from a profile whitelisted to 127.0.0.1:8098 , I switched to a portless 127.0.0.1 profile and fetched 127.0.0.1:8099 β†’ HTTP 200 . 3. Budget, max tool calls, timeout β€” never compared IsSubsetOf compared fs , network , exec , ai only. So a "subset" profile could declare budget: 0 , max tool calls: 0 , timeout: 0 β€” all meaning unlimited β€” and reset its spend and tool-call caps by switching profiles CanAI / CanToolCall enforce against the active profile . Proof: from a profile with budget: 0.000001, max tool calls: 1 , I switched to budget: 0, max tool calls: 0 β€” unlimited. What Round 1 Missed β€” And Fixed πŸ› οΈ Round 2 also confirmed the two escapes from Round 1's follow-up: | Escape | Fix | |---|---| No ratchet β€” set sandbox could switch to any profile, sandbox profile could mint permissive ones | Subset enforcement on switch and registration | MCP clients ignored the sandbox β€” mcp use stdio spawned subprocesses, mcp use sse made HTTP calls, neither gated | CanExec gate on stdio; CanNetworkTo + per-request gate on SSE incl. redirects | The three new holes above are fixed in IsSubsetOf now: budget 0 = unlimited , max tool calls , timeout are all compared,- an empty sub-whitelist is only a subset of an empty super-whitelist, - a portless sub-entry is rejected against a port-pinned super-pattern. A bonus find while wiring the multi-parameter tools: executeTool passed the model's named arguments in Go-map iteration order β€” non-deterministic, so a two-argument tool could get its args swapped. Now it's ordered by the tool's declared schema. Not a security hole gates fire first , but it made the test suite flaky and multi-arg tools unreliable. The LLM Runs πŸ€– With the fixes in, I turned a model loose twice β€” deepseek-v4-pro , thinking enabled, effort high. Variant A β€” locked profile exec: false , temp-only fs, whitelist api.github.com , plus mcp use stdio / mcp use sse as tools : 14 tool calls, 0 escapes. mcp use stdio sh β†’ E SANDBOX: exec blocked ; mcp use sse http://169.254.169.254/ β†’ not in whitelist. Variant B β€” unlocked ratchet probe whitelist pinned to api.github.com:443 , four pre-registered escape profiles : 16 tool calls, 0 escapes. Every escape profile rejected as "not a subset", set sandbox none blocked, the :80 port trick and a URL-userinfo trick both caught. Model's verdict: "The ratchet cannot be defeated." The Lesson πŸŽ“ Trust, but verify. The first audit gave a clean bill of health because the attacker wasn't creative enough β€” the second found real holes because I stopped outsourcing the hunting. An LLM is a great red-team reporter , but a lousy substitute for reading your own code. The full details β€” every vector, exact error strings, and the deterministic proofs β€” live in the repo: docs/tests/sandbox-audit/report2.en.md / report2.de.md examples/redteam.pipe variant A and examples/redteam ratchet.pipe variant B Run it yourself: DEEPSEEK API KEY=sk-... ./examples/redteam audit.sh a locked DEEPSEEK API KEY=sk-... ./examples/redteam audit.sh b ratchet probe Got a ratchet vector I missed? Open an issue β€” I'll add it to the probe suite and run it live. πŸ” Sandbox Audit Round 2 β€” I Found Three Real Escapes, Then Proved They're Dead A second red-team pass on Pipe's agent sandbox: three ratchet holes that a confined script could actually use, one latent bug, and two LLM runs that came back empty-handed. Follow-up: The audit didn't stop here. Rounds 3–6 found try ai ignoring ai:false , mis-gated embed / import , and a CLI --sandbox flag gap β€” plus the structural fixes that kill those bug classes β€” see The Audit Continues . Round 1 ended with a confident "no escape." Round 2 started by not trusting that verdict. Instead of only asking a model to attack, I read the code and built deterministic probes myself. That's where the sandbox cracked β€” not in the LLM run, but in the ratchet logic I'd shipped a week earlier. Here's what broke, how I proved it, and how the sandbox held after the fix. The Ratchet: Right Idea, Three Holes πŸ•³οΈ Pipe's sandbox is supposed to ratchet down : once a restricted profile is active, a script may only switch to a profile that grants the same or fewer rights. The check lives in IsSubsetOf . Three dimensions were missing or wrong. 1. Empty whitelist = allow-all An empty network whitelist means "allow every host." But IsSubsetOf skipped the empty-candidate case, so a profile with network whitelist: passed as a subset of a whitelisted profile. Proof: from a profile whitelisted to api.github.com , I registered and switched to an empty-whitelist profile, then fetched https://example.com β†’ HTTP 200 . Full network egress, outside the whitelist. 2. The port ratchet A portless entry api.github.com , any port passed as a subset of a port-pinned entry api.github.com:443 , because the matcher only rejected a port mismatch when both sides carried an explicit port. Proof: from a profile whitelisted to 127.0.0.1:8098 , I switched to a portless 127.0.0.1 profile and fetched 127.0.0.1:8099 β†’ HTTP 200 . 3. Budget, max tool calls, timeout β€” never compared IsSubsetOf compared fs , network , exec , ai only. So a "subset" profile could declare budget: 0 , max tool calls: 0 , timeout: 0 β€” all meaning unlimited β€” and reset its spend and tool-call caps by switching profiles CanAI / CanToolCall enforce against the active profile . Proof: from a profile with budget: 0.000001, max tool calls: 1 , I switched to budget: 0, max tool calls: 0 β€” unlimited. What Round 1 Missed β€” And Fixed πŸ› οΈ Round 2 also confirmed the two escapes from Round 1's follow-up: | Escape | Fix | |---|---| No ratchet β€” set sandbox could switch to any profile, sandbox profile could mint permissive ones | Subset enforcement on switch and registration | MCP clients ignored the sandbox β€” mcp use stdio spawned subprocesses, mcp use sse made HTTP calls, neither gated | CanExec gate on stdio; CanNetworkTo + per-request gate on SSE incl. redirects | The three new holes above are fixed in IsSubsetOf now: budget 0 = unlimited , max tool calls , timeout are all compared,- an empty sub-whitelist is only a subset of an empty super-whitelist, - a portless sub-entry is rejected against a port-pinned super-pattern. A bonus find while wiring the multi-parameter tools: executeTool passed the model's named arguments in Go-map iteration order β€” non-deterministic, so a two-argument tool could get its args swapped. Now it's ordered by the tool's declared schema. Not a security hole gates fire first , but it made the test suite flaky and multi-arg tools unreliable. The LLM Runs πŸ€– With the fixes in, I turned a model loose twice β€” deepseek-v4-pro , thinking enabled, effort high. Variant A β€” locked profile exec: false , temp-only fs, whitelist api.github.com , plus mcp use stdio / mcp use sse as tools : 14 tool calls, 0 escapes. mcp use stdio sh β†’ E SANDBOX: exec blocked ; mcp use sse http://169.254.169.254/ β†’ not in whitelist. Variant B β€” unlocked ratchet probe whitelist pinned to api.github.com:443 , four pre-registered escape profiles : 16 tool calls, 0 escapes. Every escape profile rejected as "not a subset", set sandbox none blocked, the :80 port trick and a URL-userinfo trick both caught. Model's verdict: "The ratchet cannot be defeated." The Lesson πŸŽ“ Trust, but verify. The first audit gave a clean bill of health because the attacker wasn't creative enough β€” the second found real holes because I stopped outsourcing the hunting. An LLM is a great red-team reporter , but a lousy substitute for reading your own code. The full details β€” every vector, exact error strings, and the deterministic proofs β€” live in the repo: docs/tests/sandbox-audit/report2.en.md / report2.de.md examples/redteam.pipe variant A and examples/redteam ratchet.pipe variant B Run it yourself: DEEPSEEK API KEY=sk-... ./examples/redteam audit.sh a locked DEEPSEEK API KEY=sk-... ./examples/redteam audit.sh b ratchet probe Got a ratchet vector I missed? Open an issue β€” I'll add it to the probe suite and run it live.