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

Le registre faisait passer des petits papiers

Check Point a publié le 8 septembre une recherche montrant que deux conteneurs d'exécution de code de ChatGPT, tournant sous deux comptes différents et sans aucune route réseau entre eux, pouvaient se transmettre des données via l'instance JFrog Artifactory interne qui leur servait les paquets. Rien n'a cédé sur la porte d'entrée. L'identifiant de lecture que portaient ces conteneurs pouvait aussi écrire : un champ de métadonnées sur un paquet stocké a donc fait office de boîte aux lettres. Bromure Agentic Coding ne donne à l'invité aucun compte sur le miroir, et vous laisse écrire read-only dans une règle que le fil applique.

L'isolation a tenu, les connecteurs se sont comportés comme documenté, et le service au milieu avait été installé pour la sécurité au départ. Le courrier de la victime a tout de même atteint le compte d'un inconnu, par un champ de métadonnées sur un paquet stocké.

En juin, des chercheurs de Check Point ont écrit une propriété sur un fichier dans un dépôt de paquets. La propriété s'appelait chatgpt_test_ts et sa valeur était un horodatage. Ils l'ont écrite depuis l'intérieur du bac à sable qui exécute du code pour une conversation ChatGPT. Puis ils ont ouvert une conversation sous un autre compte, dans son propre conteneur, et ont relu la même propriété. La valeur est arrivée intacte.

Cet horodatage, c'est toute la découverte. Check Point l'a publiée le 8 septembre sous un titre qui vaut mieux que n'importe quel résumé : The Shared Clipboard Inside the Sandbox. OpenAI avait déjà retiré le service à ce moment-là. Les chercheurs ont signalé la faille fin juin et se sont vu répondre que l'instance Artifactory interne avait été démantelée : il n'y a donc aucun correctif à appliquer et rien à faire pour un utilisateur. La forme, elle, demeure.

Le service au milieu

Une conversation ChatGPT qui exécute du code obtient son propre conteneur, isolé des conteneurs des autres conversations. Cette partie a fonctionné. Un conteneur qui exécute Python doit aussi pouvoir faire pip install, et une équipe plate-forme prudente ne laissera pas un bac à sable joindre pypi.org par lui-même. Le bac à sable reçoit donc un intermédiaire contrôlé : une instance interne de JFrog Artifactory qui se place devant les registres publics et sert vers l'intérieur des paquets validés.

C'est une bonne conception. Une banque place la même chose devant npm, et un acteur de santé régulé la place devant PyPI. Le miroir existe pour réduire l'exposition du bac à sable, et il le fait.

Il donne aussi à chaque conteneur un compte sur le miroir.

L'API REST d'Artifactory comprend un groupe Item Management, joignable à /api/storage/{repoKey}/{itemPath}, qui permet à un client d'attacher à un élément stocké des propriétés nommées, de type chaîne et arbitraires, puis de les relire : Set Item Properties et Get Storage Item Information. Écrire une propriété exige la permission Annotate. La lire, non. Check Point a constaté que l'identifiant délivré aux conteneurs du bac à sable, prévu comme lecteur, portait les deux. Comme Cybersecurity News l'a résumé, « les identifiants de lecture remis aux conteneurs avaient assez de permissions pour à la fois écrire et lire ces propriétés », et « ces propriétés n'étaient pas isolées par compte ».

Les valeurs de propriété acceptent du texte, ou du Base64 pour tout ce qui est binaire, et une charge trop grosse pour une seule propriété se découpe sur plusieurs et se réassemble de l'autre côté. Deux conteneurs appartenant à des clients sans lien entre eux, sans aucune route réseau les reliant, disposent désormais d'un canal bidirectionnel dont le plafond de débit ne mérite pas d'être mentionné.

deux comptes, aucune route entre eux, un service dont ils ont tous deux besoincompte de l’attaquantconteneur d’exécution de codeidentifiant lecteur + Annotatecompte de la victimeconteneur d’exécution de codeidentifiant lecteur + Annotatel’isolation tientaucun réseau, aucun disque partagéJFrog Artifactory interne — le miroir de paquets validés/api/storage/{repoKey}/{itemPath}un paquet stocké, une propriété nomméechatgpt_test_ts = “…” · Base64, découpé si volumineuxles propriétés ne sont pas cloisonnées par compteSet Item PropertiesGet Storage Item Info
Le canal caché. Deux conversations ChatGPT sous des comptes différents reçoivent chacune un conteneur d'exécution de code isolé, sans route réseau entre eux. Les deux conteneurs doivent joindre la même instance Artifactory interne pour installer des paquets, et l'identifiant de lecture qui leur a été délivré portait aussi la permission Annotate. Le conteneur A écrit une propriété nommée sur un élément stocké ; le conteneur B la relit. Les propriétés n'étaient pas cloisonnées par compte, si bien que le plan de métadonnées du miroir de paquets a servi de boîte aux lettres partagée.

Deux conversations dans le même tour

Les instructions de l'attaquant atteignent la victime par des voies ordinaires : un prompt collé, un lien de conversation partagé, ou un GPT personnalisé dont l'utilisateur ne voit pas les instructions de configuration. La victime envoie ensuite un message normal, et c'est là tout le déclencheur.

En mode Thinking, le modèle a traité deux flux de travail indépendants en un seul tour. Dans la démonstration de Check Point, la victime posait une question de cuisine et recevait une réponse correcte, tandis qu'à côté de cette réponse le modèle lisait le compte Gmail connecté de la victime et écrivait ce qu'il y trouvait dans une propriété Artifactory, à charge pour le conteneur de l'attaquant de venir la récolter. Pedro Drimel Neto, qui dirige l'équipe d'analyse de malwares de Check Point, a déclaré à The Register : « Une instruction fabriquée pouvait amener ChatGPT à traiter un second flux de tâches à côté de la conversation visible. » Gmail était la démonstration. Google Drive, Microsoft Teams et GitHub se trouvaient à la même portée.

La victime a vu un seul signal, une petite étiquette « Talked to Gmail », et elle est apparue après coup. Les connecteurs sont par défaut réglés sur Important actions, qui approuve sans demander les lectures à faible risque ; le plus strict Always ask aurait produit une invite. Ce réglage classe une lecture de courrier comme à faible risque sans se demander qui l'a demandée.

La description que Check Point donne du modèle : il « peut donc agir comme un initié contraint : non pas malveillant, mais persuadé d'agir pour le compte de l'attaquant ».

La partie qui se généralise

Retirez ChatGPT et l'histoire parle d'un agent de code, parce que la structure est identique.

Un bac à sable d'agent doit installer des paquets. Le laisser joindre les registres publics sans intermédiaire est précisément ce que vous vouliez éviter, alors vous placez un miroir devant : Artifactory, Nexus, un proxy npm privé, ce sur quoi votre organisation s'est standardisée. Le bac à sable s'authentifie désormais auprès de ce miroir, il détient donc un identifiant, et quelqu'un a décidé ce que cet identifiant pouvait faire bien avant que cet agent existe. Les registres de paquets acceptent des envois, des tags, des annotations et des règles de rétention, ce qui fait d'Annotate une petite permission posée sur une large surface.

Posez donc deux questions à votre propre installation, plutôt que de demander si le bac à sable est isolé. Nommez ce sur quoi il détient un compte. Puis nommez quels verbes fonctionnent sur ce compte.

Dans un espace de travail Bromure

Bromure Agentic Coding exécute chaque agent de code dans une VM Linux virtualisée par le matériel sur votre Mac, et garde les vrais secrets du côté hôte de cette ligne. Faites passer les trois ingrédients de Check Point par un espace de travail.

Un service partagé. Il n'y en a aucun. Chaque espace de travail correspond à une VM, avec son propre disque système, sa propre image de home, sa propre IP et son propre écouteur proxy MITM sur l'hôte. Deux espaces de travail ne partagent aucune VM, et la description qu'en donne le manuel pour une paire d'entre eux est qu'ils « ne partagent jamais un octet ». Aucun miroir à l'échelle du parc ne se trouve sous les espaces de travail avec un plan de métadonnées dans lequel les deux peuvent écrire. Ce qui se trouve sous un espace de travail, c'est un processus sur votre propre Mac, un par espace de travail.

Un identifiant sur le miroir. L'agent n'en détient aucun. Les installations de paquets dans un espace de travail Bromure sortent par un socket virtio vers le proxy de l'hôte, et c'est le proxy qui parle au registre. Bromure garde les clés d'API des services de réputation côté hôte et ne les exporte dans aucune VM. Le chemin Depi est le cas le plus net : le proxy attache votre clé de registre comme en-tête Authorization: Bearer sur l'hôte et supprime au passage tout en-tête Authorization envoyé par l'invité. Aucun identifiant de lecture n'existe dans l'invité, donc aucun ne peut porter un verbe de trop.

Un verbe d'écriture qui a survécu à sa raison d'être. Ici, c'est vous qui écrivez la contrainte. Le panneau Guardrails d'un espace de travail porte un pare-feu de sortie ordonné, et les règles pour le protocole web acceptent une colonne Methods. Sous allow, les méthodes forment une liste d'autorisation, et read-only est un raccourci pour GET, HEAD et OPTIONS. Le manuel dit pourquoi cette colonne existe : la dimension des méthodes « est ce qui permet à un espace de travail de lire une API qu'il n'a pas le droit d'écrire », sur le fil, quel que soit l'outil qui émet la requête à l'intérieur de la VM. 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, et le proxy apparie de nouveau par nom de serveur TLS et par méthode HTTP individuelle, si bien qu'un agent compromis n'a de route pour contourner ni l'un ni l'autre. Les modifications atteignent les sessions en cours sans redémarrage, et chaque verdict se dépose comme une ligne Firewall dans la Security Timeline.

la décision prise en amontconteneur bac à sablejeton lecteur + Annotateportée fixée une fois, jadisGET + PUTmiroir partagé — une instance, tous les locatairesles paquets sur le plan des artefacts, les boîtes aux lettres sur celui des métadonnéesce que le jeton peut faire n’a jamais été une question par sessionla décision prise sur le fil, par espace de travailVM d’espace de travailaucun identifiant de registreleurres seulement, brm-…aucune autre route vers le réseauvsock 8443proxy hôte — un par espace de travail, sur votre Macsupprimer l’Authorization de l’invité · attacher la vraie cléallow web registry.npmjs.org read-onlyapparié par nom de serveur TLS et méthode HTTP, à chaque requête
La même exigence, deux emplacements pour la décision. En haut : le bac à sable s'authentifie auprès d'un miroir partagé, et ce qu'il peut y faire a été décidé en amont, une fois, dans une attribution de permission qui a survécu à la personne qui l'a faite. En bas : l'invité ne détient aucun identifiant de registre et aucune route vers le réseau hormis un socket virtio vers un proxy hôte propre à l'espace de travail, où la clé est attachée, où l'en-tête Authorization de l'invité est supprimé, et où une règle ordonnée décide quelles méthodes HTTP sont autorisées à atteindre l'hôte.

Le second flux, et le chemin du retour

Le second flux de tâches de l'attaquant est arrivé comme du contenu que le modèle a lu. Dans un espace de travail Bromure, tout ce qu'un agent ingère et renvoie en flux au modèle (contenus de fichiers, pages web, sorties de commandes, tout ce que retourne un outil) voyage sous forme de spans tool_result à travers ce même proxy hôte. Le détecteur de code source note ces spans avec un classifieur PromptGuard local avant que le modèle n'agisse dessus, puis journalise la détection, se met en pause pour votre décision, ou bloque la requête. Le détecteur est livré désactivé et télécharge un modèle de 298 Mo la première fois que vous l'activez : activez-le donc avant la session où vous en aurez besoin.

La lecture de connecteur non approuvée a ici son inverse. Les jetons bearer MCP d'un espace de travail Bromure apparaissent dans la VM comme des marque-places brm-mcp_. Le vrai jeton reste hors de l'invité, et le proxy le substitue sur le fil en direction du seul hôte pour lequel ce jeton a été émis. Activez Ask before use pour un identifiant et sa première utilisation dans une session se met en pause pour un dialogue côté hôte offrant une autorisation de 5 minutes, d'une heure, ou du reste de la session. C'est l'invite que « Important actions » a refusé d'afficher, par identifiant, sur la machine devant laquelle vous êtes assis.

Si quelque chose dans la VM emporte un marque-place là où il n'a rien à faire, le proxy refuse la requête sans transmettre un octet, met la VM en pause, et lève une alerte nommant l'aperçu du jeton, l'identifiant, l'hôte pour lequel il a été émis et l'hôte vers lequel il se dirigeait. Sous ce fil-piège, le traçage réglé sur Activity only écrit une ligne de métadonnées par requête : hôte, méthode, chemin, statut, octets, quels identifiants le proxy a échangés, et un avertissement de fuite pour tout jeton de type bearer que Bromure n'a pas émis. Lancez bromure-cli trace hostnames après une session et chaque hôte distinct contacté par l'agent s'affiche avec un compteur.

Si vous exploitez un miroir pour vos agents

Vérifiez l'identité avec laquelle vos bacs à sable s'authentifient, avant le chemin réseau entre eux. Sur Artifactory, cherchez Annotate sur un jeton que vous pensez être un lecteur, et une cible de permission plus large que les dépôts depuis lesquels le bac à sable installe. L'API de propriétés est un plan de métadonnées inscriptible ; les règles de rétention, les tags et les informations de build en sont d'autres. Un lecteur qui peut annoter peut poster.

La règle qui répond en une ligne

Dans le panneau Guardrails de l'espace de travail, ajoutez une règle de sortie pour le registre avec le protocole web et read-only dans la colonne Methods, puis réglez Unmatched traffic sur Deny et listez ce dont le travail a besoin. Le dépliant au format pf en bas du panneau imprime ce que vous avez construit : allow web registry.npmjs.org read-only, allow web api.github.com, default deny. Cela s'applique aux sessions en cours sans redémarrage.

Un service partagé fait des locataires des voisins

L'instinct, après une histoire comme celle-ci, est d'aller vérifier les murs. Les murs allaient bien. Deux conteneurs sous deux comptes n'avaient aucune route l'un vers l'autre, et Check Point n'en a trouvé aucune. Ils ont trouvé un tiers auquel les deux conteneurs avaient le droit de parler, et l'ont utilisé comme on utilise des disques partagés et des enregistrements DNS TXT depuis trente ans.

Check Point énonce la leçon dans l'article : « un service interne partagé, pensé purement comme de l'infrastructure, peut devenir une couche de communication involontaire entre des environnements censés rester isolés ». Une infrastructure que vous installez pour la sécurité reste une infrastructure, et ses locataires sont à un saut l'un de l'autre. Réduisez l'ensemble des choses sur lesquelles un bac à sable détient un compte, et déplacez la décision sur ce que ces comptes peuvent faire vers chaque requête, hors d'un ticket de provisionnement que quelqu'un a fermé il y a deux ans.

Un bac à sable d'agent qui vaut la peine d'être exécuté aura un chemin contrôlé vers les paquets dont il a besoin, parce que l'alternative est pire. Traitez ce chemin comme une relation à laquelle sont attachées des permissions, et traitez ces permissions comme votre périmètre. Décidez-les à un endroit où vous pouvez encore les voir. Installez Bromure Agentic Coding et ne donnez à votre agent rien avec quoi se connecter.