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.“
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.
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.