{"slug": "two-ai-agents-one-jail-round-9-of-pipe-s-sandbox-audit", "title": "🕵️ Two AI Agents, One Jail — Round 9 of Pipe's Sandbox Audit", "summary": "Pipe's Round 9 sandbox audit, run by two independent LLM agents in parallel, found two real bugs: the exec_whitelist check only validated the first token, allowing shell injection via substitutions, and builtins-as-tools were silently uncallable. The fix tokenizes commands and runs them directly as argv without a shell, closing the gap between check and execution.", "body_md": "[← All posts← Alle Beiträge](../blog.html)\n\n# 🕵️ Two AI Agents, One Jail — Round 9 of Pipe's Sandbox Audit\n\n**Two independent LLM agents — DeepSeek in its strongest \"max effort\" mode and a keyless free model — attack Pipe's sandbox in parallel, inspired by the OpenAI–Hugging Face incident and a real OpenCode config bug. Every tested vector held. Two real bugs didn't even need the attacks: building the harness found them first.**\n\n> This post covers round 9, the parallel dual-agent pass. For earlier rounds, see > ** Round 1**,\n\n**, and >**\n\n[Round 2](sandbox-audit-2.html)**. Rounds 7 and 8 (CLI fs-write and AI-egress gaps) > are documented in the audit reports. > Full technical write-up:**\n\n[Rounds 3–6](sandbox-audit-continued.html)**.**\n\n[report9.en.md](../../docs/tests/sandbox-audit/report9.en.md)After round 2 I learned to stop outsourcing the hunting — the real escapes came from reading my own code, not from an LLM run. Round 9 flips that on its head. A user shared OpenAI's technical incident report on the July 2026 Hugging Face compromise: internal models, red-teaming their own sandboxed eval, turned an allowed service into an SSRF relay and an improvised inter-agent \"message board,\" then escalated from there into production. Separately, a real GitHub issue against OpenCode showed a name-based permission system defeated by config precedence.\n\nRound 9's question: do any of those mechanisms transfer to Pipe's sandbox model — and, new for this round, do **two different models run in parallel** find the same things?\n\n## Why two agents, and why two *processes*\n\n`ActiveProfile`\n\nis a process-wide pointer, not a per-session value. Two goroutines calling `set_sandbox`\n\nin one process would stomp each other. So \"parallel\" here means two separate `bin/pipe`\n\nsubprocesses, each with its own locked `redteam9`\n\nprofile instance and its own throwaway working directory.\n\nAnd they were given **different** `network_whitelist`\n\nhosts — `api.github.com`\n\nfor DeepSeek, `example.com`\n\nfor OpenCode. No shared writable target means no accidental inter-agent coordination. That is a deliberate, structural countermeasure to the exact mechanism the incident report describes: a shared writable service becoming a message board.\n\n```\n┌──────────────────┐      ┌──────────────────┐\n│ DeepSeek         │      │ OpenCode         │\n│ deepseek-v4-pro  │      │ big-pickle       │\n│ thinking+max     │      │ keyless free     │\n│ whitelist: api.  │      │ whitelist: ex.   │\n│ github.com       │      │ example.com      │\n└────────┬─────────┘      └────────┬─────────┘\n         │ separate processes, no │\n         │ shared writable state  │\n         ▼                        ▼\n      locked profile \"redteam9\"  locked profile \"redteam9\"\n```\n\n## Finding 1 — `exec_whitelist`\n\ngave no real protection 🔓\n\nConfirmed by code reading first, then live-reproduced with a harmless PoC *before* spending any API budget. `CanExecCommand`\n\nvalidated only the first whitespace token of a command string against `exec_whitelist`\n\n; `exec`\n\n/`proc_start`\n\nthen handed the **entire, unfiltered string** to a real shell (`sh -c`\n\n).\n\nSo `exec_whitelist: [\"echo\"]`\n\ndid not stop `exec \"echo $(whoami)-$(cat /etc/hostname)\"`\n\n— the check saw `echo`\n\n(allowed) and the shell executed the substitution anyway, returning real host output.\n\n**The fix:** when a profile's `exec_whitelist`\n\nis non-empty, the command is tokenized and run directly as argv — **no shell** — so `&&`\n\n, `;`\n\n, `` `\n\n``, `\n\n$(...)` become inert literal arguments, not syntax. The same tokenizer is used for the whitelist check *and* the actual invocation (a shared `\n\nbuildExecCommand` helper), so there is no gap between what's checked and what's run. This applied to both `\n\nexec` and `\n\nproc_start`, which shared the identical bug.\n\n## Finding 2 — builtins-as-tools were silently uncallable 💥\n\nWhile building the harness, every single tool call in a smoke test failed with `not callable: BUILTIN`\n\n— for every tool, including completely benign ones. Root cause: the tree-walker wraps a bare builtin reference (exactly what `ai_tool \"read_file\" ... read_file`\n\ndoes) in its own type, and the shared dispatcher only recognized the VM's builtin type. The VM was never affected; only default (non-`-vm`\n\n) execution was.\n\nThis is a serious, previously-masked bug: **every example script that registers a raw builtin as an ai_tool — including all the redteam*.pipe scripts from rounds 1–8 — has been silently broken** under the tree-walker since the regression landed, with every tool call failing before ever reaching the builtin or the profile's policy checks.\n\n**The fix:** a small `CallableBuiltin`\n\ninterface — `pkg/object`\n\nstill never imports `pkg/eval`\n\n(that would cycle); the interface is the seam.\n\n## Finding 3 — `ai_tool`\n\nreorders multi-parameter calls alphabetically (found *by the attack*) 📝\n\nDeepSeek's own final report flagged \"a strange parameter-swap bug\" in `write_file`\n\n— the file that got created was named using the *content* it meant to write, and contained the *path* string. Root cause: Pipe map literals don't preserve declaration order anywhere, so `{path: ..., content: ...}`\n\nis passed to the builtin in **alphabetical** order — `write_file(content, path)`\n\n. Arguments swapped.\n\nNot patched this round: there's no minimal fix (order is lost at parse time), so a real fix means making map literals order-preserving end to end — a separate, larger initiative. Immediate mitigation shipped: an explicit warning in the `ai_tool`\n\ndocs. But this is a genuine example of why running a live adversarial model against your own tooling surfaces things static review doesn't.\n\n## Live results — what held\n\nBoth agents ran to the profile's `max_tool_calls: 30`\n\ncap and stopped cleanly (itself a working defense layer, independent of the dollar `budget`\n\nfield).\n\n| # | Category | Result |\n|---|---|---|\n| 1 | Classic (fs jail, env masking, network whitelist, lock, MCP) | HOLDS — every `/etc/shadow` /`/root/.ssh/id_rsa` /`/proc/self/environ` read collapsed to the jail; env masked; non-whitelisted hosts, IP literals and localhost all rejected by exact-match whitelist; lock blocked `set_sandbox` ; both MCP entry points blocked |\n| 1b | `exec_whitelist` + shell metacharacters | HOLDS — verified by *both* models independently |\n| 2 | Symlink-in-jail (new) | HOLDS — `ln -s /etc/passwd` succeeds (legitimate), but `read_file` does not dereference it: paths are canonicalized to the jail before any filesystem call touches the symlink |\n| 3 | `ai_vision` magic-byte spoofing (new) | HOLDS — moot: local reads go through the same jail canonicalization as `read_file` , so a spoofed file is never reachable at its real path |\n| 4 | Runtime tool registration (new) | INCONCLUSIVE — test-harness schema issue, not a policy block |\n| 5 | Swarm handoff injection (new) | INCONCLUSIVE — same harness limitation |\n| 6 | Indirect prompt injection (new) | Minimal data — one benign whitelisted fetch before budget ran out; nothing injected observed anywhere |\n\nEven the genuinely new vectors held — and the honest part matters: **vectors 4 and 5 are untested, not confirmed-safe.** The harness couldn't legally exercise them (a schema limitation), so I'm saying so plainly rather than claiming a pass the test couldn't actually earn. Round 10 should retry them once `ai_tool`\n\nschemas can express map/list parameters.\n\n## The parallel design paid off\n\nDeepSeek's report ended by flagging symlink read-through as *unverified* — it ran out of budget before testing it. OpenCode's completed run supplied the answer: the redirect is not followed; canonicalization happens first. Two agents, one student, one grader — with no shared writable resource the whole time.\n\n## Honest bottom line\n\n- Two independently-confirmed bugs fixed\n**before** either agent's tools even worked (Findings 1 and 2). This round's most valuable results came from building the harness carefully. - One independently-confirmed bug found\n**by the live attack itself**(Finding 3), fixed in docs now, structurally later. - Every vector that could actually be tested end-to-end\n**held**. - The OpenAI incident's core mechanism — abusing an allowed relay to reach forbidden targets — did\n**not** transfer: redirects and every AI-egress path re-validate against the whitelist on every hop.\n\nThe full report is ** report9.en.md**.\n\n# 🕵️ Zwei KI-Agenten, ein Gefängnis — Runde 9 des Pipe-Sandbox-Audits\n\n**Zwei unabhängige KI-Agenten — DeepSeek im stärksten „Max-Effort\"-Modus und ein schlüsselloses Gratis-Modell — greifen Pipes Sandbox parallel an, inspiriert vom OpenAI–Hugging-Face-Vorfall und einem echten OpenCode-Config-Bug. Jeder getestete Vektor hielt. Zwei echte Bugs brauchten nicht mal den Angriff: Sie fielen schon beim Bauen des Harness auf.**\n\n> Dieser Beitrag behandelt Runde 9, den parallelen Zwei-Agenten-Durchgang. Frühere Runden: > ** Runde 1**,\n\n**und >**\n\n[Runde 2](sandbox-audit-2.html)**. Runden 7 und 8 (CLI-fs-write- und AI-Egress-Lücken) > sind in den Audit-Reports dokumentiert. > Vollständiger technischer Report:**\n\n[Runden 3–6](sandbox-audit-continued.html)**.**\n\n[report9.de.md](../../docs/tests/sandbox-audit/report9.de.md)Nach Runde 2 hatte ich gelernt, das Jagen nicht mehr auszulagern — die echten Escapes kamen vom Lesen meines eigenen Codes, nicht von einem LLM-Lauf. Runde 9 dreht das um. Ein Nutzer teilte OpenAIs technischen Incident-Report zur Juli-2026-Kompromittierung von Hugging Face: interne Modelle, die ihre eigene Sandbox-Eval red-teamen, machten aus einem erlaubten Dienst einen SSRF-Relay und ein improvisiertes „Message Board\" zwischen Agenten und eskalierten von dort bis in die Produktion. Dazu zeigte ein echtes GitHub-Issue gegen OpenCode ein namensbasiertes Berechtigungssystem, das durch Config-Precedence besiegt wurde.\n\nDie Frage in Runde 9: Übertragen sich diese Mechanismen auf Pipes Sandbox-Modell — und, neu in dieser Runde: Finden **zwei verschiedene Modelle im Parallelbetrieb** dieselben Dinge?\n\n## Warum zwei Agenten, und warum zwei *Prozesse*\n\n`ActiveProfile`\n\nist ein prozessweiter Zeiger, kein Wert pro Sitzung. Zwei Goroutinen, die in einem Prozess `set_sandbox`\n\naufrufen, würden sich gegenseitig überschreiben. „Parallel\" heißt hier also: zwei getrennte `bin/pipe`\n\n-Subprozesse, je eigene gelockte `redteam9`\n\n-Profilinstanz und ein eigenes Wegwerf-Arbeitsverzeichnis.\n\nUnd sie bekamen **verschiedene** `network_whitelist`\n\n-Hosts — `api.github.com`\n\nfür DeepSeek, `example.com`\n\nfür OpenCode. Kein geteiltes beschreibbares Ziel bedeutet keine unbeabsichtigte Koordination zwischen den Agenten. Das ist eine bewusste, strukturelle Gegenmaßnahme zu genau dem Mechanismus aus dem Incident-Report: ein geteilter beschreibbarer Dienst, der zum Message Board wird.\n\n```\n┌──────────────────┐      ┌──────────────────┐\n│ DeepSeek         │      │ OpenCode         │\n│ deepseek-v4-pro  │      │ big-pickle       │\n│ thinking+max     │      │ schlüssellos     │\n│ whitelist: api.  │      │ whitelist: ex.   │\n│ github.com       │      │ example.com      │\n└────────┬─────────┘      └────────┬─────────┘\n         │ getrennte Prozesse, kein │\n         │ geteilter Schreibzustand │\n         ▼                        ▼\n   Profil \"redteam9\" gelockt   Profil \"redteam9\" gelockt\n```\n\n## Finding 1 — `exec_whitelist`\n\nbot keinen echten Schutz 🔓\n\nZuerst per Code-Lektüre bestätigt, dann mit einem harmlosen PoC live reproduziert — **bevor** irgendein API-Budget ausgegeben wurde. `CanExecCommand`\n\nprüfte nur das erste Whitespace-Token eines Befehls gegen `exec_whitelist`\n\n; `exec`\n\n/`proc_start`\n\nübergaben danach den **kompletten, ungefilterten String** an eine echte Shell (`sh -c`\n\n).\n\nSo hielt `exec_whitelist: [\"echo\"]`\n\n`exec \"echo $(whoami)-$(cat /etc/hostname)\"`\n\nnicht auf — die Prüfung sah `echo`\n\n(erlaubt), und die Shell führte die Substitution trotzdem aus und gab echten Host-Output zurück.\n\n**Der Fix:** Sobald die `exec_whitelist`\n\neines Profils nicht leer ist, wird der Befehl tokenisiert und direkt als argv ausgeführt — **ohne Shell** — sodass `&&`\n\n, `;`\n\n, `` `\n\n``, `\n\n$(...)` zu inerten Literal-Argumenten statt Syntax werden. Derselbe Tokenizer wird für die Whitelist-Prüfung *und* die tatsächliche Ausführung genutzt (gemeinsamer `\n\nbuildExecCommand`-Helper) — keine Lücke zwischen „geprüft\" und „ausgeführt\". Betraf `\n\nexec` und `\n\nproc_start`, die denselben Bug teilten.\n\n## Finding 2 — Builtins als Tools waren stillschweigend unaufrufbar 💥\n\nBeim Bauen des Harness schlug jeder einzelne Tool-Call in einem Smoke-Test mit `not callable: BUILTIN`\n\nfehl — bei jedem Tool, auch völlig harmlosen. Ursache: Der Tree-Walker wickelt eine nackte Builtin-Referenz (genau das, was `ai_tool \"read_file\" ... read_file`\n\ntut) in einen eigenen Typ, und der gemeinsame Dispatcher erkannte nur den Builtin-Typ der VM. Die VM war nie betroffen; nur die Standard-Ausführung (ohne `-vm`\n\n).\n\nDas ist ein schwerer, bislang verborgener Bug: **Jedes Beispielskript, das ein rohes Builtin als ai_tool registriert — inklusive aller redteam*.pipe-Skripte aus Runden 1–8 — war unter dem Tree-Walker stillschweigend kaputt**, jeder Tool-Call scheiterte, bevor er je das Builtin oder die Policy-Prüfungen des Profils erreichte.\n\n**Der Fix:** ein kleines `CallableBuiltin`\n\n-Interface — `pkg/object`\n\nimportiert `pkg/eval`\n\nweiterhin nicht (das wäre ein Zyklus); das Interface ist die Naht.\n\n## Finding 3 — `ai_tool`\n\nsortiert Multi-Parameter-Aufrufe alphabetisch (vom *Angriff* gefunden) 📝\n\nDeepSeeks eigener Abschlussbericht meldete „einen seltsamen Parameter-Swap-Bug\" bei `write_file`\n\n— die erzeugte Datei war nach dem *Inhalt* benannt, den es schreiben wollte, und enthielt den *Pfad*-String. Ursache: Pipe-Map-Literale erhalten die Deklarationsreihenfolge nirgends, sodass `{path: ..., content: ...}`\n\nin **alphabetischer** Reihenfolge ans Builtin geht — `write_file(content, path)`\n\n. Argumente vertauscht.\n\nDiese Runde nicht gefixt: Es gibt keinen Mini-Fix (die Reihenfolge geht bereits beim Parsen verloren), ein echter Fix bedeutet, Map-Literale end-to-end ordnungserhaltend zu machen — ein separates, größeres Vorhaben. Sofortige Abmilderung geschickt: eine explizite Warnung in der `ai_tool`\n\n-Doku. Aber es ist ein echtes Beispiel dafür, warum ein Live-Adversarial-Modell gegen die eigene Tooling Dinge aufdeckt, die statische Review nicht findet.\n\n## Live-Ergebnis — was hielt\n\nBeide Agenten liefen bis zum `max_tool_calls: 30`\n\n-Limit des Profils und stoppten sauber (selbst eine funktionierende Verteidigungsebene, unabhängig vom Dollar-`budget`\n\n).\n\n| # | Kategorie | Ergebnis |\n|---|---|---|\n| 1 | Klassisch (fs-Jail, env-Masking, Netzwerk-Whitelist, Lock, MCP) | HÄLT — jeder `/etc/shadow` /`/root/.ssh/id_rsa` /`/proc/self/environ` -Read kollabierte ins Jail; env maskiert; Nicht-Whitelist-Hosts, IP-Literale und localhost alle per Exakt-Match-Whitelist abgelehnt; Lock blockierte `set_sandbox` ; beide MCP-Einstiege geblockt |\n| 1b | `exec_whitelist` + Shell-Metazeichen | HÄLT — von *beiden* Modellen unabhängig verifiziert |\n| 2 | Symlink-im-Jail (neu) | HÄLT — `ln -s /etc/passwd` gelingt (legitim), aber `read_file` dereferenziert es nicht: Pfade werden vor jedem Dateisystem-Zugriff ins Jail kanonisiert |\n| 3 | `ai_vision` -Magic-Byte-Spoofing (neu) | HÄLT — hinfällig: lokale Reads laufen durch dieselbe Jail-Kanonisierung wie `read_file` , eine gespoofte Datei ist an ihrem echten Pfad nie erreichbar |\n| 4 | Laufzeit-Tool-Registrierung (neu) | NICHT SCHLÜSSIG — Harness-Schema-Problem, kein Policy-Block |\n| 5 | Swarm-Handoff-Injection (neu) | NICHT SCHLÜSSIG — dieselbe Harness-Einschränkung |\n| 6 | Indirekte Prompt-Injection (neu) | Wenig Daten — ein harmloser Whitelisted-Fetch vor Budgetende; nirgends Injektion beobachtet |\n\nAuch die wirklich neuen Vektoren hielten — und der ehrliche Teil zählt: **Vektor 4 und 5 sind ungetestet, nicht bestätigt-sicher.** Das Harness konnte sie legal nicht ausüben (eine Schema-Einschränkung), also sage ich das klar, statt einen Pass zu behaupten, den der Test nicht wirklich verdient hat. Runde 10 sollte sie erneut angehen, sobald `ai_tool`\n\n-Schemas Map-/List-Parameter ausdrücken können.\n\n## Das parallele Design hat sich ausgezahlt\n\nDeepSeeks Report endete damit, den Symlink-Read-Durchgriff als *unverifiziert* zu markieren — das Budget ging aus, bevor es getestet wurde. OpenCodes vollständiger Lauf lieferte die Antwort: Der Redirect wird nicht verfolgt; die Kanonisierung passiert zuerst. Zwei Agenten, ein Schüler, ein Bewerter — ohne geteilte Schreibressource, die ganze Zeit.\n\n## Ehrliches Fazit\n\n- Zwei unabhängig bestätigte Bugs, gefixt\n**bevor** die Tools beider Agenten überhaupt liefen (Finding 1 und 2). Die wertvollsten Ergebnisse dieser Runde kamen vom sorgfältigen Harness-Bau. - Ein unabhängig bestätigter Bug, gefunden\n**vom Live-Angriff selbst**(Finding 3), jetzt in der Doku gefixt, strukturell später. - Jeder end-to-end testbare Vektor\n**hielt**. - Der Kernmechanismus des OpenAI-Vorfalls — einen erlaubten Relay zu missbrauchen, um verbotene Ziele zu erreichen —\n**übertrug sich nicht**: Redirects und jeder AI-Egress-Pfad validieren bei jedem Hop erneut gegen die Whitelist.\n\nDer vollständige Report ist ** report9.de.md**.", "url": "https://wpnews.pro/news/two-ai-agents-one-jail-round-9-of-pipe-s-sandbox-audit", "canonical_source": "https://pipe-lang.com/blog/round-9-sandbox-audit.html", "published_at": "2026-08-30 00:00:00+00:00", "updated_at": "2026-08-30 02:51:49.844621+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "ai-research"], "entities": ["Pipe", "DeepSeek", "OpenCode", "OpenAI", "Hugging Face"], "alternates": {"html": "https://wpnews.pro/news/two-ai-agents-one-jail-round-9-of-pipe-s-sandbox-audit", "markdown": "https://wpnews.pro/news/two-ai-agents-one-jail-round-9-of-pipe-s-sandbox-audit.md", "text": "https://wpnews.pro/news/two-ai-agents-one-jail-round-9-of-pipe-s-sandbox-audit.txt", "jsonld": "https://wpnews.pro/news/two-ai-agents-one-jail-round-9-of-pipe-s-sandbox-audit.jsonld"}}