🕵️ Two AI Agents, One Jail — Round 9 of Pipe's Sandbox Audit 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. ← All posts← Alle Beiträge ../blog.html 🕵️ Two AI Agents, One Jail — Round 9 of Pipe's Sandbox Audit 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. This post covers round 9, the parallel dual-agent pass. For earlier rounds, see Round 1 , , and 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: Rounds 3–6 sandbox-audit-continued.html . 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. Round 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? Why two agents, and why two processes ActiveProfile is a process-wide pointer, not a per-session value. Two goroutines calling set sandbox in one process would stomp each other. So "parallel" here means two separate bin/pipe subprocesses, each with its own locked redteam9 profile instance and its own throwaway working directory. And they were given different network whitelist hosts — api.github.com for DeepSeek, example.com for 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. ┌──────────────────┐ ┌──────────────────┐ │ DeepSeek │ │ OpenCode │ │ deepseek-v4-pro │ │ big-pickle │ │ thinking+max │ │ keyless free │ │ whitelist: api. │ │ whitelist: ex. │ │ github.com │ │ example.com │ └────────┬─────────┘ └────────┬─────────┘ │ separate processes, no │ │ shared writable state │ ▼ ▼ locked profile "redteam9" locked profile "redteam9" Finding 1 — exec whitelist gave no real protection 🔓 Confirmed by code reading first, then live-reproduced with a harmless PoC before spending any API budget. CanExecCommand validated only the first whitespace token of a command string against exec whitelist ; exec / proc start then handed the entire, unfiltered string to a real shell sh -c . So exec whitelist: "echo" did not stop exec "echo $ whoami -$ cat /etc/hostname " — the check saw echo allowed and the shell executed the substitution anyway, returning real host output. The fix: when a profile's exec whitelist is non-empty, the command is tokenized and run directly as argv — no shell — so && , ; , , $ ... become inert literal arguments, not syntax. The same tokenizer is used for the whitelist check and the actual invocation a shared buildExecCommand helper , so there is no gap between what's checked and what's run. This applied to both exec and proc start , which shared the identical bug. Finding 2 — builtins-as-tools were silently uncallable 💥 While building the harness, every single tool call in a smoke test failed with not callable: BUILTIN — 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 does in its own type, and the shared dispatcher only recognized the VM's builtin type. The VM was never affected; only default non- -vm execution was. This 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. The fix: a small CallableBuiltin interface — pkg/object still never imports pkg/eval that would cycle ; the interface is the seam. Finding 3 — ai tool reorders multi-parameter calls alphabetically found by the attack 📝 DeepSeek's own final report flagged "a strange parameter-swap bug" in write file — 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: ...} is passed to the builtin in alphabetical order — write file content, path . Arguments swapped. Not 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 docs. But this is a genuine example of why running a live adversarial model against your own tooling surfaces things static review doesn't. Live results — what held Both agents ran to the profile's max tool calls: 30 cap and stopped cleanly itself a working defense layer, independent of the dollar budget field . | | Category | Result | |---|---|---| | 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 | | 1b | exec whitelist + shell metacharacters | HOLDS — verified by both models independently | | 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 | | 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 | | 4 | Runtime tool registration new | INCONCLUSIVE — test-harness schema issue, not a policy block | | 5 | Swarm handoff injection new | INCONCLUSIVE — same harness limitation | | 6 | Indirect prompt injection new | Minimal data — one benign whitelisted fetch before budget ran out; nothing injected observed anywhere | Even 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 schemas can express map/list parameters. The parallel design paid off DeepSeek'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. Honest bottom line - Two independently-confirmed bugs fixed 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 by the live attack itself Finding 3 , fixed in docs now, structurally later. - Every vector that could actually be tested end-to-end held . - The OpenAI incident's core mechanism — abusing an allowed relay to reach forbidden targets — did not transfer: redirects and every AI-egress path re-validate against the whitelist on every hop. The full report is report9.en.md . 🕵️ Zwei KI-Agenten, ein Gefängnis — Runde 9 des Pipe-Sandbox-Audits 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. Dieser Beitrag behandelt Runde 9, den parallelen Zwei-Agenten-Durchgang. Frühere Runden: Runde 1 , und 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: Runden 3–6 sandbox-audit-continued.html . 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. Die 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? Warum zwei Agenten, und warum zwei Prozesse ActiveProfile ist ein prozessweiter Zeiger, kein Wert pro Sitzung. Zwei Goroutinen, die in einem Prozess set sandbox aufrufen, würden sich gegenseitig überschreiben. „Parallel" heißt hier also: zwei getrennte bin/pipe -Subprozesse, je eigene gelockte redteam9 -Profilinstanz und ein eigenes Wegwerf-Arbeitsverzeichnis. Und sie bekamen verschiedene network whitelist -Hosts — api.github.com für DeepSeek, example.com fü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. ┌──────────────────┐ ┌──────────────────┐ │ DeepSeek │ │ OpenCode │ │ deepseek-v4-pro │ │ big-pickle │ │ thinking+max │ │ schlüssellos │ │ whitelist: api. │ │ whitelist: ex. │ │ github.com │ │ example.com │ └────────┬─────────┘ └────────┬─────────┘ │ getrennte Prozesse, kein │ │ geteilter Schreibzustand │ ▼ ▼ Profil "redteam9" gelockt Profil "redteam9" gelockt Finding 1 — exec whitelist bot keinen echten Schutz 🔓 Zuerst per Code-Lektüre bestätigt, dann mit einem harmlosen PoC live reproduziert — bevor irgendein API-Budget ausgegeben wurde. CanExecCommand prüfte nur das erste Whitespace-Token eines Befehls gegen exec whitelist ; exec / proc start übergaben danach den kompletten, ungefilterten String an eine echte Shell sh -c . So hielt exec whitelist: "echo" exec "echo $ whoami -$ cat /etc/hostname " nicht auf — die Prüfung sah echo erlaubt , und die Shell führte die Substitution trotzdem aus und gab echten Host-Output zurück. Der Fix: Sobald die exec whitelist eines Profils nicht leer ist, wird der Befehl tokenisiert und direkt als argv ausgeführt — ohne Shell — sodass && , ; , , $ ... zu inerten Literal-Argumenten statt Syntax werden. Derselbe Tokenizer wird für die Whitelist-Prüfung und die tatsächliche Ausführung genutzt gemeinsamer buildExecCommand -Helper — keine Lücke zwischen „geprüft" und „ausgeführt". Betraf exec und proc start , die denselben Bug teilten. Finding 2 — Builtins als Tools waren stillschweigend unaufrufbar 💥 Beim Bauen des Harness schlug jeder einzelne Tool-Call in einem Smoke-Test mit not callable: BUILTIN fehl — 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 tut 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 . Das 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. Der Fix: ein kleines CallableBuiltin -Interface — pkg/object importiert pkg/eval weiterhin nicht das wäre ein Zyklus ; das Interface ist die Naht. Finding 3 — ai tool sortiert Multi-Parameter-Aufrufe alphabetisch vom Angriff gefunden 📝 DeepSeeks eigener Abschlussbericht meldete „einen seltsamen Parameter-Swap-Bug" bei write file — 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: ...} in alphabetischer Reihenfolge ans Builtin geht — write file content, path . Argumente vertauscht. Diese 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 -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. Live-Ergebnis — was hielt Beide Agenten liefen bis zum max tool calls: 30 -Limit des Profils und stoppten sauber selbst eine funktionierende Verteidigungsebene, unabhängig vom Dollar- budget . | | Kategorie | Ergebnis | |---|---|---| | 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 | | 1b | exec whitelist + Shell-Metazeichen | HÄLT — von beiden Modellen unabhängig verifiziert | | 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 | | 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 | | 4 | Laufzeit-Tool-Registrierung neu | NICHT SCHLÜSSIG — Harness-Schema-Problem, kein Policy-Block | | 5 | Swarm-Handoff-Injection neu | NICHT SCHLÜSSIG — dieselbe Harness-Einschränkung | | 6 | Indirekte Prompt-Injection neu | Wenig Daten — ein harmloser Whitelisted-Fetch vor Budgetende; nirgends Injektion beobachtet | Auch 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 -Schemas Map-/List-Parameter ausdrücken können. Das parallele Design hat sich ausgezahlt DeepSeeks 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. Ehrliches Fazit - Zwei unabhängig bestätigte Bugs, gefixt 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 vom Live-Angriff selbst Finding 3 , jetzt in der Doku gefixt, strukturell später. - Jeder end-to-end testbare Vektor hielt . - Der Kernmechanismus des OpenAI-Vorfalls — einen erlaubten Relay zu missbrauchen, um verbotene Ziele zu erreichen — übertrug sich nicht : Redirects und jeder AI-Egress-Pfad validieren bei jedem Hop erneut gegen die Whitelist. Der vollständige Report ist report9.de.md .