← Retour à tous les articles
Publié le · par Renaud Deraison

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

La frontière et ce qu'elle borne, en un seul espaceun processus Node, un tas V8contexte de confiancepeut demander au parent d'agirdétient le jetoncontexte non fiableexécute le code du dépôtc'est le bac à sablele tas qu'ils partagent tous deux"a1f3…-…-…" le jeton d'autorisation, une chaînev8.getHeapSnapshot()ce que fait le côté non fiable1. capturer le tas, garder les UUID2. "non autorisé" vs erreur de validation3. forger des requêtes sur le pipe stdoutle processus parentjeton valide → exécution, hors bac à sablecommandes sur le Macdepuis read-only, sans invite
Heapjack ne brise pas le bac à sable ; il en lit la clé. Le contexte de confiance, le contexte non fiable et le jeton d'autorisation qui les distingue vivent tous dans un seul tas V8, à l'intérieur d'un seul processus Node. v8.getHeapSnapshot() est une API de profilage publique qui renvoie toutes les chaînes de ce tas. Le contexte non fiable collecte les candidats en forme d'UUID et identifie le vrai jeton à la différence entre une réponse 'non autorisé' et une erreur de validation, puis forge des requêtes de confiance sur le pipe stdout partagé. Le processus parent valide le jeton et exécute la commande hors du bac à sable, en mode read-only, là où rien n'est censé être inscriptible.

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

un bac à sable dans le processusun seul système, un seul compte utilisateurl'agentqui lit leur dépôtla politiqueun jeton dans le même tas~/.ssh ~/.aws ~/.zshrc trousseau vos dépôtsune chaîne lue en mémoireet la séparation est finie,tout ce qui précède est à portéeune frontière à l'hyperviseurl'invité Linux, non fiable, root comprisl'agent, le dépôt, son propre $HOMEghp_… sk-ant-… leurresVirtualization.framework + vsockle Mac, où se prennent les décisionsle proxyvraies clés, règles,le 451 à la sortiela boîte de dialogue"Autoriser l'écriture … ?"rendue hors de la VM
Ce qui compte, c'est quelle machine évalue les règles. À gauche : le garde, le code gardé, le jeton d'autorisation, vos clés SSH, vos identifiants cloud et votre .zshrc se trouvent tous dans un même système d'exploitation, si bien que lire une seule chaîne met fin à la séparation. À droite : l'agent reçoit une VM Linux dont les identifiants sont des leurres qui préservent la structure, dont le commutateur virtuel détourne les flux sur les ports 80 et 443 vers un proxy hôte que l'invité ne peut pas désactiver, et dont le pire résultat est un disque que l'on efface. Une compromission totale de l'invité, root compris, rapporte à l'attaquant 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.