← Zurück zu allen Beiträgen
Veröffentlicht am · von Renaud Deraison

Das Token lag im selben Heap

Oren Yomtov von Accomplish ist zweimal aus der Codex-Sandbox von OpenAI ausgebrochen. Ein Ausbruch lief aus read-only heraus, dem striktesten Modus: nicht vertrautes JavaScript rief v8.getHeapSnapshot() auf und las das Autorisierungs-Token des vertrauten Kontexts aus dem Heap, den sich beide teilten. Der andere brachte apply_patch dazu, sein eigenes Schreibrecht bis zur Wurzel des Dateisystems auszuweiten, und hängte dann eine Zeile an .zshrc an. OpenAI hat beides in acht Tagen behoben. Für die Eigenschaft, die beides hervorgebracht hat, gibt es keinen Patch: ein Wächter und der Code, den er bewacht, waren zwei Variablen in einem Prozess. Bromure Agentic Coding lässt den Wächter auf deinem Mac laufen und den Agenten in einer VM.

Der nicht vertraute Kontext hat die Sandbox nie durchbrochen. Er bat Node um einen Schnappschuss des Heaps, fand dort das Autorisierungs-Token der Sandbox als ganz gewöhnlichen String liegen, und benutzte es.

Du richtest Codex auf ein Repository, das dir jemand geschickt hat. Du bist vorsichtig, also startest du es in read-only, dem Modus, in dem der Agent schauen und nicht anfassen darf. Der Agent liest den Code. Ein Teil dieses Codes ist eine Nutzlast, und ein paar Sekunden später läuft ein Befehl auf deinem Mac, außerhalb der Sandbox, ohne eine einzige Abfrage auf dem Bildschirm.

Das ist Heapjack, einer von zwei Ausbrüchen aus der Codex-Sandbox, die Oren Yomtov von Accomplish diesen Monat veröffentlicht hat. BleepingComputer hat am 20. September darüber berichtet. Yomtov meldete beide am 12. August an OpenAI, und OpenAI behob sie binnen acht Tagen: Heapjack in Codex Desktop Build 26.818.21641, den anderen in Codex CLI 0.149.0. Die Stellungnahme von OpenAI ist von der kurzen Sorte: „Wir danken den Forschern dafür, dass sie uns kontaktiert und ihre Erkenntnisse geteilt haben. Wir haben beide Probleme im August behoben, aber wir härten unsere Sandboxes fortlaufend weiter.“

Warum der erste Bug überhaupt existierte, das ist der Teil, der den Patch überdauert.

Ein Prozess, ein Heap, ein Token

Codex Desktop hat ein Werkzeug namens node_repl, das JavaScript ausführt. Es hält darin zwei Kontexte: einen vertrauten, der den Elternprozess um echte Handlungen bitten darf, und einen nicht vertrauten, der ausführt, was der Agent gerade auswertet. Die Trennung zwischen beiden ist die Sandbox.

Beide Kontexte leben im selben Node-Prozess. In Yomtovs Worten: „Ein Node-Prozess, ein V8-Isolate, ein Heap, den sich beide Kontexte teilen. Das Token ist ein String in diesem Heap.“

Das Token ist die Art, wie der Elternprozess die beiden auseinanderhält. Eine Anfrage, die mit dem richtigen Token über die Pipe eintrifft, gilt als vertraut und wird ausgeführt; eine ohne nicht. Es ist eine zufällige UUID, was ein vollkommen brauchbares Geheimnis ist, solange der Code, vor dem du es verbirgst, deinen Speicher nicht lesen kann.

Er kann. v8.getHeapSnapshot() ist eine öffentliche Node-API, ein Profiling-Werkzeug, und sie liefert einen Abzug jedes Objekts im Heap, Strings eingeschlossen. Der nicht vertraute Kontext ruft sie auf, filtert auf alles, was wie eine UUID aussieht, und hat am Ende eine Handvoll Kandidaten. Den echten herauszufischen kostet einen Schritt mehr: jeden beim Parent ausprobieren und schauen, was zurückkommt. Ein falsches Token antwortet „nicht autorisiert“. Das richtige antwortet mit einer Beschwerde über den Rest der Anfrage. Yomtov behält den String, der die zweite Antwort erzeugt.

Mit dem Token in der Hand schreibt der nicht vertraute Kontext Anfragen auf dieselbe stdout-Pipe, die auch der vertraute benutzt. Der Parent prüft das Token, findet es gültig, und führt den Befehl ohne Sandbox aus. Von da an: Systembefehle über open, der Docker-Socket, Änderungen an Konfigurationsdateien.

All das aus read-only heraus. Noch einmal Yomtov: „Alles davon lief in read-only, dem striktesten Sandbox-Modus, in dem der Agent angeblich nichts schreiben kann.“

Die Grenze und das Begrenzte, in einem Adressraumein Node-Prozess, ein V8-Heapvertrauter Kontextdarf den Parent handeln lassenhält das Tokennicht vertrauter Kontextführt den Code des Repos ausdas ist die Sandboxder Heap, den beide teilen"a1f3…-…-…" das Autorisierungs-Token, als Stringv8.getHeapSnapshot()was die nicht vertraute Seite tut1. Heap-Snapshot, UUID-Strings behalten2. "nicht autorisiert" vs Validierungsfehler3. Anfragen auf der stdout-Pipe fälschender ElternprozessToken gültig → ausführen, ohne SandboxBefehle auf dem Macaus read-only, ohne Abfrage
Heapjack bricht die Sandbox nicht auf; es liest den Schlüssel der Sandbox. Der vertraute Kontext, der nicht vertraute Kontext und das Autorisierungs-Token, das sie unterscheidet, leben alle in einem einzigen V8-Heap innerhalb eines einzigen Node-Prozesses. v8.getHeapSnapshot() ist eine öffentliche Profiling-API, die jeden String in diesem Heap zurückgibt. Der nicht vertraute Kontext sammelt die UUID-förmigen Kandidaten und erkennt das echte Token am Unterschied zwischen einer Antwort 'nicht autorisiert' und einem Validierungsfehler, dann fälscht er vertraute Anfragen auf der gemeinsamen stdout-Pipe. Der Elternprozess validiert das Token und führt den Befehl außerhalb der Sandbox aus, im Modus read-only, wo nichts beschreibbar sein sollte.

Ein Schreibrecht, das wächst

Overpatch, der zweite Ausbruch, ist schlichter. Codex CLI hat ein Werkzeug apply_patch zum Bearbeiten von Dateien, und es leitet aus den Pfaden, die im übergebenen Patch genannt werden, ab, wie viel Schreibzugriff es braucht. Die Pfade kommen vom Modell, und das Modell liest gerade das Repository.

Also nennt ein Patch in einem Eintrag /tmp, mit einer Änderung, die nichts tut. Die Änderung schlägt fehl. Die Rechteerteilung, die sie impliziert hat, nicht: der Schreibzugriff weitet sich bis zur Wurzel des Dateisystems. Ein zweiter Eintrag im selben Patch schreibt daraufhin über einen Symlink nach $HOME und hängt eine Zeile an .zshrc an.

Noch ist nichts ausgebrochen. Der Ausbruch passiert später, wenn du ein Terminal öffnest und deine Shell diese Zeile als dich ausführt, ohne irgendeine Sandbox auf dem Weg. Dieser hier lief in workspace-write, dem Alltagsmodus.

Zwei verschiedene Bugs, zwei verschiedene Produkte, ein Satz aus dem Artikel, der beide abdeckt: „Das, was die Durchsetzung übernahm, saß in dem, was durchgesetzt werden sollte.“

Der Wächter und das Bewachte, in einem Adressraum

Eine Sandbox, die im selben Prozess lebt wie der Code, den sie zurückhält, ist eine Übereinkunft. Sie hält, bis jemand genau hinschaut. Das Token liegt im Heap. Die Rechteberechnung liest vom Angreifer gelieferte Pfade. Die Pipe ist dieselbe Pipe. Jedes davon ist behebbar, und OpenAI hat jedes in acht Tagen behoben. Der nächste Bug dieser Art wird ein anderer String im selben Heap sein.

Yomtovs eigene Schlussfolgerung ist die architektonische: „Genau deshalb lässt Accomplish den gesamten Agenten in einer VM laufen… Wir behandeln alles im Gast als nicht vertrauenswürdig, root eingeschlossen, weil nichts, das entscheidet, was erlaubt ist, dort drinnen mitläuft.“

Das ist der Entwurf, den Bromure Agentic Coding ausliefert. Der Agent läuft in einem Ubuntu-24.04-Gast unter Apples Virtualization.framework, dem Hypervisor, den macOS selbst mitbringt. Der Gast ist von Bauart her nicht vertrauenswürdig, root eingeschlossen. Der Code, der entscheidet, was erlaubt ist, läuft als Host-Prozess auf der anderen Seite eines vsock, und kein Token im Gast autorisiert irgendetwas, weil alles, was zu autorisieren sich lohnt, außerhalb des Gasts sitzt.

eine Sandbox im Prozessein Betriebssystem, ein Benutzerkontoder Agentliest deren Repositorydie Richtlinieein Token im selben Heap~/.ssh ~/.aws ~/.zshrc Keychain jedes Repoein String aus dem Speicherund die Trennung ist vorbei,alles oben ist in Reichweiteeine Grenze am Hypervisorder Linux-Gast, nicht vertraut, root inklusiveder Agent, das Repo, sein eigenes $HOMEghp_… sk-ant-… AttrappenVirtualization.framework + vsockder Mac, wo entschieden wirdder Proxyechte Schlüssel, Regeln,die 451 beim Hinausgehender Dialog"Schreiben auf … ?"außerhalb der VM gerendert
Worauf es ankommt, ist, welche Maschine die Regeln auswertet. Links: der Wächter, der bewachte Code, das Autorisierungs-Token und deine SSH-Schlüssel, Cloud-Zugangsdaten und die .zshrc sitzen alle in einem Betriebssystem, sodass das Lesen eines einzigen Strings die Trennung beendet. Rechts: der Agent bekommt eine Linux-VM, deren Zugangsdaten strukturerhaltende Attrappen sind, deren Verkehr auf Port 80 und Port 443 der virtuelle Switch in einen Host-Proxy umleitet, den der Gast nicht abschalten kann, und deren schlimmster Ausgang eine Platte ist, die du löschst. Die vollständige Kompromittierung des Gasts, root eingeschlossen, bringt dem Angreifer den Gast.

Worauf jeder Ausbruch stattdessen trifft

Overpatch ist der greifbare Fall, also fang damit an. Der Trick endet damit, etwas an .zshrc in $HOME anzuhängen. In einem Bromure-Workspace ist das $HOME des Gasts home.img, ein ext4-Image, das an diese VM angehängt ist. Das Home-Verzeichnis deines Macs liegt nicht darin. Was der Gast auf deinem Mac erreichen kann, sind die Projektordner, die du in ihn hineingeteilt hast: bis zu acht, von dir ausgewählt, im Workspace-Editor aufgelistet. Die Dotfiles, die deine Shell beim Öffnen des Terminals liest, sind nicht darunter, es sei denn, du legst sie dort hinein.

Heapjack ist der schwierigere Fall. Nichts in Bromure erkennt einen Heap-Snapshot-Trick im Inneren des Gasts, und nichts soll das. Was sich ändert, ist, wo der Ausbruch ankommt. In Codex landete der ausgebrochene Code in dem Konto, das deine SSH-Schlüssel und deine Cloud-Zugangsdaten hält. In einem Bromure-Workspace landet er in einer Linux-VM, in der:

Die Zugangsdaten sind Attrappen

sk-ant-api03-brm-…, ein String in ghp_-Form, ein ~/.aws-Profil, dessen Secret ungültig ist. Sie sind strukturerhaltend, also akzeptieren Werkzeuge sie, und sie sind stromaufwärts wertlos. Die echten Werte leben im Speicher des Proxys auf dem Host und werden auf dem Weg nach draußen eingesetzt, begrenzt auf genau den Host oder seine Subdomains.

Eine hinauszutragen löst den Draht aus

Der Proxy durchsucht Header und Bodies jeder ausgehenden Anfrage nach den eigenen Attrappen des Workspace. Eine Attrappe, die irgendwohin unterwegs ist, wofür sie nicht geprägt wurde, wird mit HTTP 451 blockiert, ohne dass ein einziges Byte weitergeleitet wird. Die VM pausiert, und eine rote Zeile Credential-Brokering erscheint in der Security Timeline.

Das Netz ist eine Liste, die du geschrieben hast

Egress ist ein geordnetes Regelwerk, das auf default allow oder default deny endet. Der virtuelle Switch leitet den Verkehr der VM auf Port 80 und Port 443 in den Host-Proxy um, ohne jede Umgebungsvariable und ohne irgendetwas, das der Gast abschalten könnte. Änderungen greifen sofort in einer laufenden Sitzung.

Ein Push ist eine Frage, gestellt auf dem Mac

Zugangsdaten für Git-Forges tragen eine Schreibrichtlinie, und neue Workspaces stehen standardmäßig auf Vor dem Schreiben fragen. git push ist ein Schreibvorgang. Der Dialog benennt die genaue Operation und wird auf deinem Desktop gerendert. Ein Angreifer in der VM kann ihn nicht anklicken.

Wenn es doch schiefgeht, erholst du dich mit einem Menüpunkt. Der Kompromittierungs-Ablauf löscht Platte und Home des Workspace und behält deine Einstellungen, Tokens und SSH-Schlüssel. Die echten Zugangsdaten haben deinen Mac nie verlassen, du rotierst also nichts. Platte zurücksetzen klont das System-Image neu. Keine Bromure-Aktion rührt deine geteilten Projektordner an, und die Abfrage sagt das.

Der strikteste Modus war der, der verlor

Die Entwicklerin in dieser Geschichte hat alles richtig gemacht. Sie hat absichtlich die strikteste Einstellung gewählt, die das Werkzeug anbietet, weil das Repository nicht ihres war. read-only ist eine echte Funktion und hat echte Arbeit geleistet, bis ein String in einem geteilten Heap sie in einen Vorschlag verwandelt hat.

Modi, Allowlists und Freigabeabfragen sind es wert, da zu sein, und jede einzelne von ihnen setzt voraus, dass der Code, der sie durchsetzt, außerhalb der Reichweite des Codes sitzt, den er einschränkt. Das ist eine Behauptung über Speicherlayout, und du kannst sie nachprüfen gehen. Yomtov hat Codex zweimal nachgeprüft, über zwei Produkte hinweg, und die Antwort kam beide Male mit Nein zurück.

Also lass den Agenten dort laufen, wo die Antwort aufhört, eine Rolle zu spielen. Gib ihm root, lass ihn installieren, was er mag, lass ihn das Repository lesen, das dir jemand geschickt hat, und halte jede Zugangsdatei, jede Zielentscheidung und jede Push-Bestätigung auf der anderen Seite eines Hypervisors, wo der nächste Ausbruch in einer Maschine ankommt, die du ohnehin schon bereit warst wegzuwerfen.