cd /news/ai-safety/sandbox-audit-round-2-three-real-esc… Β· home β€Ί topics β€Ί ai-safety β€Ί article
[ARTICLE Β· art-96409] src=pipe-lang.com β†— pub= topic=ai-safety verified=true sentiment=Β· neutral

πŸ” 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.

read8 min views1 publishedAug 12, 2026
πŸ” Sandbox Audit Round 2 β€” Three Real Escapes, Then Zero
Image: Pipe-Lang (auto-discovered)

← All posts← Alle BeitrΓ€ge

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) andexamples/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.

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) andexamples/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.

── more in #ai-safety 4 stories Β· sorted by recency
── more on @pipe 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/sandbox-audit-round-…] indexed:0 read:8min 2026-08-12 Β· β€”