Le jeton était dans le même tas
Oren Yomtov, d'Accomplish, s'est échappé deux fois du bac à sable de Codex, chez OpenAI. L'une des évasions partait de read-only, le mode le plus strict : du JavaScript non fiable a appelé v8.getHeapSnapshot() et a lu le jeton d'autorisation du contexte de confiance dans le tas que tous deux partageaient. L'autre a fait élargir à apply_patch son propre droit d'écriture jusqu'à la racine du système de fichiers, puis a ajouté une ligne à .zshrc. OpenAI a corrigé les deux en huit jours. La propriété qui les a produites n'a pas de correctif : un garde et le code qu'il garde étaient deux variables dans un seul processus. Bromure Agentic Coding fait tourner le garde sur votre Mac et l'agent dans une VM.
Le contexte non fiable n'a jamais brisé le bac à sable. Il a demandé à Node un instantané du tas, y a trouvé le jeton d'autorisation du bac à sable posé là comme une chaîne ordinaire, et s'en est servi.
Vous pointez Codex sur un dépôt que quelqu'un vous a envoyé. Vous êtes prudent,
alors vous le lancez en read-only, le mode où l'agent peut regarder sans
toucher. L'agent lit le code. Une partie de ce code est une charge utile, et
quelques secondes plus tard une commande s'exécute sur votre Mac, hors du bac à
sable, sans aucune invite à l'écran.
C'est Heapjack, l'une des deux évasions du bac à sable de Codex qu'Oren Yomtov, d'Accomplish, a publiées ce mois-ci. BleepingComputer les a couvertes le 20 septembre. Yomtov a signalé les deux à OpenAI le 12 août, et OpenAI a corrigé sous huit jours : Heapjack dans Codex Desktop build 26.818.21641, l'autre dans Codex CLI 0.149.0. La déclaration d'OpenAI est du genre bref : « Nous remercions les chercheurs de nous avoir contactés et d'avoir partagé leurs conclusions. Nous avons traité les deux problèmes en août, mais nous renforçons continuellement nos bacs à sable. »
Pourquoi le premier bug existait, voilà la part qui survit au correctif.
Un processus, un tas, un jeton
Codex Desktop dispose d'un outil nommé node_repl qui exécute du JavaScript. Il
y tient deux contextes : un de confiance, autorisé à demander au processus
parent d'accomplir des choses réelles, et un non fiable, qui exécute ce que
l'agent est en train d'évaluer. La séparation entre les deux, c'est le bac à
sable.
Les deux contextes vivent dans le même processus Node. Selon les mots de Yomtov : « Un processus Node, un isolat V8, un tas partagé par les deux contextes. Le jeton est une chaîne dans ce tas. »
Le jeton est ce par quoi le processus parent distingue l'un de l'autre. Une requête qui arrive sur le pipe avec le bon jeton est de confiance et se fait exécuter ; une requête sans jeton, non. C'est un UUID aléatoire, ce qui est un très bon secret tant que le code auquel vous le cachez ne peut pas lire votre mémoire.
Il le peut. v8.getHeapSnapshot() est une API Node publique, un outil de
profilage, et elle renvoie un vidage de chaque objet du tas, chaînes comprises.
Le contexte non fiable l'appelle, filtre ce qui a la forme d'un UUID, et se
retrouve avec une poignée de candidats. Isoler le vrai demande une étape de
plus : essayer chacun auprès du parent et observer ce qui revient. Un mauvais
jeton répond « non autorisé ». Le bon répond par une plainte à propos du reste de
la requête. Yomtov garde la chaîne qui produit la seconde réponse.
Le jeton en main, le contexte non fiable écrit des requêtes sur le même pipe
stdout que celui du contexte de confiance. Le parent vérifie le jeton, le trouve
valide, et exécute la commande hors bac à sable. À partir de là : des commandes
système via open, la socket Docker, des modifications de fichiers de
configuration.
Tout cela depuis read-only. Yomtov, encore : « Tout cela tournait en
read-only, le mode de bac à sable le plus strict, où l'agent est censé ne rien
pouvoir écrire. »
Un droit d'écriture qui grandit
Overpatch, la seconde évasion, est plus simple. Codex CLI a un outil
apply_patch pour modifier des fichiers, et il déduit l'ampleur de l'accès en
écriture dont il a besoin des chemins nommés dans le correctif qu'on lui a
remis. Les chemins viennent du modèle, et le modèle est en train de lire le
dépôt.
Alors un correctif nomme /tmp dans une entrée, avec une modification qui ne
fait rien. La modification échoue. Pas l'autorisation qu'elle impliquait :
l'accès en écriture s'élargit jusqu'à la racine du système de fichiers. Une
seconde entrée du même correctif écrit ensuite à travers un lien symbolique dans
$HOME et ajoute une ligne à .zshrc.
Rien ne s'est encore échappé. L'évasion survient plus tard, quand vous ouvrez un
terminal et que votre shell exécute cette ligne en votre nom, sans le moindre
bac à sable sur le chemin. Celle-ci tournait en workspace-write, le mode de
tous les jours.
Deux bugs différents, deux produits différents, une phrase de l'article qui couvre les deux : « Ce qui assurait l'application des règles était assis à l'intérieur de ce à quoi ces règles s'appliquaient. »
Le garde et le gardé, dans un même espace d'adressage
Un bac à sable qui vit dans le même processus que le code qu'il contient est une convention. Elle tient jusqu'à ce que quelqu'un la regarde de près. Le jeton est dans le tas. Le calcul des permissions lit des chemins fournis par l'attaquant. Le pipe est le même pipe. Chacun de ces points est réparable, et OpenAI a réparé chacun en huit jours. Le prochain bug de ce genre sera une autre chaîne dans le même tas.
La conclusion de Yomtov est, elle, architecturale : « C'est pour cela qu'Accomplish fait tourner l'agent entier à l'intérieur d'une VM… Nous traitons tout ce qui se trouve dans l'invité comme non fiable, root compris, parce que rien de ce qui décide de ce qui est permis n'y tourne avec lui. »
C'est la conception que livre Bromure Agentic Coding. L'agent tourne dans un invité Ubuntu 24.04 sous Virtualization.framework d'Apple, l'hyperviseur que macOS fournit lui-même. L'invité est non fiable par construction, root compris. Le code qui décide de ce qui est permis tourne comme un processus hôte de l'autre côté d'un vsock, et aucun jeton à l'intérieur de l'invité n'autorise quoi que ce soit, parce que tout ce qui vaut la peine d'être autorisé se trouve hors de l'invité.
Ce que chaque évasion rencontre à la place
Overpatch est la plus concrète, alors commençons par elle. L'astuce se termine en
ajoutant du texte à .zshrc dans $HOME. Dans un espace de travail Bromure, le
$HOME de l'invité est home.img, une image ext4 attachée à cette VM. Le
répertoire personnel de votre Mac n'est pas dedans. Ce que l'invité peut
atteindre sur votre Mac, ce sont les dossiers de projet que vous y avez
partagés : jusqu'à huit, choisis par vous, listés dans l'éditeur d'espace de
travail. Les fichiers de configuration que votre shell lit à l'ouverture du
Terminal n'en font pas partie, sauf si vous les y mettez.
Heapjack est le cas le plus ardu. Rien dans Bromure ne détecte une astuce d'instantané du tas à l'intérieur de l'invité, et ce n'est pas le but. Ce qui change, c'est l'endroit où l'évasion arrive. Dans Codex, le code échappé atterrissait dans le compte qui détient vos clés SSH et vos identifiants cloud. Dans un espace de travail Bromure, il atterrit dans une VM Linux où :
Les identifiants sont des leurres
sk-ant-api03-brm-…, une chaîne en forme de ghp_, un profil ~/.aws dont
le secret est invalide. Ils préservent la structure, donc les outils les
acceptent, et ils ne valent rien en amont. Les vraies valeurs vivent dans la
mémoire du proxy sur l'hôte et sont substituées à la sortie, cantonnées à
l'hôte exact ou à son sous-domaine.
En emporter un déclenche l'alarme
Le proxy inspecte les en-têtes et les corps de chaque requête sortante à la recherche des leurres propres à l'espace de travail. Un leurre qui part ailleurs que là où il a été frappé est bloqué par un HTTP 451, sans qu'un seul octet soit transmis. La VM se met en pause, et une ligne rouge Courtage d'identifiants apparaît dans la Chronologie de sécurité.
Le réseau est une liste que vous avez écrite
La sortie est un jeu de règles ordonné qui se termine par default allow ou
default deny. Le commutateur virtuel détourne les flux de la VM sur les
ports 80 et 443 vers le proxy hôte, sans qu'aucune variable d'environnement
n'intervienne et sans rien que l'invité puisse désactiver. Les modifications
s'appliquent immédiatement à une session en cours.
Un push est une question, posée sur le Mac
Les identifiants de forge Git portent une politique d'écriture, et les
nouveaux espaces de travail utilisent par défaut Demander avant
d'écrire. git push est une écriture. La boîte de dialogue nomme
l'opération exacte et s'affiche sur votre bureau. Un attaquant à l'intérieur
de la VM ne peut pas cliquer dessus.
Quand cela tourne mal, vous récupérez avec un élément de menu. Le flux de compromission efface le disque et le home de l'espace de travail et conserve vos réglages, vos jetons et vos clés SSH. Les vrais identifiants n'ont jamais quitté votre Mac, vous n'avez donc rien à faire tourner. Réinitialiser le disque re-clone l'image système. Aucune action Bromure ne touche à vos dossiers de projet partagés, et l'invite le dit.
Le mode le plus strict est celui qui a perdu
Le développeur de cette histoire a tout fait comme il faut. Il a choisi le
réglage le plus strict que l'outil propose, exprès, parce que le dépôt n'était
pas le sien. read-only est une vraie fonctionnalité et elle a fait un vrai
travail, jusqu'à ce qu'une chaîne dans un tas partagé la transforme en
suggestion.
Les modes, les listes d'autorisation et les invites d'approbation valent la peine d'exister, et chacun d'eux suppose que le code qui les applique est hors de portée du code qu'il contraint. C'est une affirmation sur la disposition de la mémoire, et vous pouvez aller la vérifier. Yomtov a vérifié Codex deux fois, sur deux produits, et la réponse est revenue négative les deux fois.
Alors faites tourner l'agent là où la réponse cesse d'avoir de l'importance. Donnez-lui root, laissez-le installer ce qu'il veut, laissez-le lire le dépôt que quelqu'un vous a envoyé, et gardez chaque identifiant, chaque décision de destination et chaque confirmation de push de l'autre côté d'un hyperviseur, là où la prochaine évasion arrive dans une machine que vous étiez déjà prêt à jeter.