L'aperçu pouvait lire votre dossier personnel
Le 14 octobre, Noma Labs a dévoilé SneakPeek, une faille du volet d'aperçu de Claude Code Desktop. Un fichier HTML gonflé au-delà de 256 Ko, déguisé en rapport de couverture de tests, était chargé depuis le disque sans politique de scripts, et son JavaScript lisait les clés SSH, les secrets AWS, les kubeconfigs et les propres jetons OAuth de Claude dans le dossier personnel du développeur, avant de les envoyer au-dehors par une balise en arrière-plan. Anthropic l'a notée CVSS 8.7 et l'a corrigée en août. Dans Bromure Agentic Coding, les pages s'affichent dans une VM séparée et jetable qui ne contient aucun de vos fichiers, et le dossier personnel de l'agent ne contient que des substituts au lieu de clés.
Le dépôt avait l'air normal. Le fichier était un rapport de couverture. Le développeur a demandé à l'agent d'y jeter un œil, et le volet d'aperçu a fait le reste.
Un développeur clone un dépôt et demande à Claude Code de regarder le rapport de couverture de tests qu'il contient. Une demande banale. Le rapport est un fichier HTML. Quand Claude lit ou modifie un fichier HTML dans Claude Code Desktop, un volet d'aperçu l'affiche à côté de la conversation, sans rien demander. Ce rapport contient un script. En quelques secondes, le script a lu la clé privée SSH du développeur, ses clés AWS, ses jetons npm et GitHub, ses identifiants Kubernetes et les propres jetons OAuth de Claude, et a tout envoyé à un serveur contrôlé par l'attaquant.
C'est la preuve de concept que Noma Labs a publiée le mercredi 14 octobre, sous le nom de SneakPeek. CSO Online l'a couverte le jour même. Anthropic a noté la faille CVSS 8.7 (élevée) et a considéré son impact comme équivalent à une exécution de code sur l'hôte. Noma l'avait signalée le 4 juillet. Le correctif est arrivé par mise à jour automatique le 17 août, dans Claude Desktop 1.30096.1, après qu'un premier correctif a été contourné. Un avis GitHub et un CVE sont en attente.
Le bug est corrigé. La configuration qui le rendait intéressant à trouver est toujours là sur la plupart des portables de développeurs : quelque chose qui affiche du contenu non fiable, qui tourne sous votre compte, à côté de tout ce que vous possédez.
Trois défenses et une limite de taille
Le volet d'aperçu est un panneau Chromium à l'intérieur de l'application Electron. Noma a compté trois défenses autour de lui, et a trouvé une brèche dans chacune.
Une politique de scripts pour les petits fichiers. Les fichiers de moins de
256 Ko étaient chargés comme URL data: en base64, avec une Content Security
Policy, une liste de règles qui indique au navigateur quels scripts ont le droit
de s'exécuter. Cette politique bloquait les scripts. Les fichiers de plus de
256 Ko étaient écrits dans un fichier temporaire puis chargés depuis le disque
comme URL file://, et la politique ne s'appliquait pas à ce chemin. Gonfler un
rapport au-delà de 256 Ko est trivial. Les vrais rapports de couverture
atteignent souvent cette taille.
Une origine avec accès à tout le disque. L'origine d'une page décide de ce
qu'elle peut lire. Une URL data: reçoit une origine opaque qui ne peut rien
lire. Une page chargée depuis file:// obtenait, selon les mots de Noma, « un
accès de même origine à l'ensemble du système de fichiers local de la
victime ». Le script déduisait le dossier personnel à partir du chemin du
fichier temporaire et lisait les fichiers avec fetch() et XMLHttpRequest.
Un filtre qui n'était pas activé. Le volet avait un filtre de requêtes censé
bloquer l'accès file://. Il s'enclenchait pour les serveurs de développement
passant par le proxy, et restait éteint pour le HTML statique affiché depuis le
disque.
Pour faire sortir les données, le script appelait navigator.sendBeacon(), une
API web qui envoie en arrière-plan une requête HTTP POST à sens unique. Rien dans
l'application ne limitait les destinations des requêtes du volet. Le compte
rendu de Noma ajoute l'adresse IP réelle du développeur, récupérée via WebRTC.
Le premier correctif d'Anthropic, le 24 juillet, bloquait fetch() et
XMLHttpRequest vers les chemins file:// depuis la page. Le jour même, Noma a
déplacé les lectures dans un Web Worker lancé depuis une URL blob:, un
contexte que le correctif ne couvrait pas, et les trois défauts fonctionnaient à
nouveau. Le correctif final d'août a fermé ensemble la brèche de la politique,
le chargement file:// et le chemin du worker. Le chercheur de Noma, Eli
Ainhorn, l'a résumé pour CSO : « Les protections étaient réelles, elles ne
couvraient simplement pas tous les chemins. »
Ce n'est pas l'agent
SneakPeek n'utilise aucune injection de prompt. L'agent a lu un fichier que vous lui avez demandé de lire, et l'application autour de lui a affiché ce fichier. Le script s'est exécuté dans le processus de rendu de l'aperçu, avec l'accès de l'application à votre disque.
Crystal Morin, de Sysdig, a expliqué à CSO que les journaux au niveau de l'agent pourraient ne pas capturer cette exécution, et que les défenseurs devraient donc surveiller les lectures de fichiers et les connexions sortantes au niveau du runtime. La transcription de votre session montrerait une demande anodine et une réponse anodine.
Un modèle prudent, des règles de permission strictes et un humain qui approuve chaque commande n'auraient rien changé. Le moteur de rendu tournait sur le Mac, sous votre compte, à côté de votre dossier personnel et de tous les identifiants que vous y avez accumulés.
Le conseil d'Ainhorn à CSO vise cette seconde partie : garder les identifiants à longue durée de vie hors des machines des développeurs, et utiliser des jetons à portée limitée et de courte durée.
Le même rapport dans une machine Bromure
Dans Bromure Agentic Coding, chaque agent tourne dans sa propre VM Linux sur votre Mac, appelée machine, et le navigateur intégré qui affiche les pages web tourne dans une seconde VM jetable, à côté. Clonez le dépôt de Noma dans une machine et demandez à Claude Code de regarder le rapport de couverture.
Lire le rapport, c'est lire du texte
L'agent lit coverage/index.html comme n'importe quel fichier : comme du texte
dans son contexte. Rien dans la conversation ne l'affiche. Si vous cliquez sur
le fichier dans le volet Files, la vue de détail montre le HTML en source
colorée, exactement comme un .py ou un .json. Aucun script de la page ne
s'exécute. Les fichiers de plus de 512 Ko ne sont pas prévisualisés du tout. Un
rapport gonflé au-delà de 256 Ko pour esquiver une politique de scripts
apparaît comme 300 Ko de balisage.
Le rendu se fait dans une autre machine
Quand vous voulez vraiment voir le rapport affiché, l'agent sert le dossier depuis sa machine et l'ouvre dans le volet du navigateur, à l'adresse IP de la machine. Le navigateur est un Chromium complet, et il tourne dans sa propre VM annexe. Comme le dit le manuel, « un fichier que l'agent écrit dans son dossier personnel ne se trouve pas sur le disque du navigateur ».
Le script s'exécute et ne trouve rien à lire. L'origine de la page est
http://192.168.64.x:8000, une origine web ordinaire. Chromium ne la laisse pas
lire les chemins file://. Un script qui passerait outre, via un worker ou un
bug du navigateur, atterrirait sur un clone éphémère en copie sur écriture de
l'image du navigateur : pas de ~/.ssh, pas de ~/.aws, pas de kubeconfig, pas
de jeton OAuth, aucun de vos projets. Le dossier personnel qu'il déduit d'un
chemin temporaire appartient à une VM Chromium vide. La balise part vide.
Par défaut, le navigateur ne garde aucun profil. Stay signed in to websites est désactivé, donc cookies et connexions disparaissent à la fermeture du navigateur, et un navigateur masqué est détruit au bout de cinq minutes. WebRTC reste désactivé tant que vous n'autorisez pas la caméra ou le micro dans les réglages Browser de la machine, si bien que l'astuce de l'adresse IP n'a rien à appeler.
Le dossier de l'agent ne contient que des substituts
Pointez maintenant le même script sur la VM de l'agent. Disons que la commande de test du dépôt écrit et ouvre elle-même le rapport, si bien que le code s'exécute là où l'agent travaille. Parcourez la liste de Noma dans ce dossier personnel :
- Clé privée SSH. Il n'y en a pas.
SSH_AUTH_SOCKrepose sur un pont d'agent vers le Mac. La VM peut lister les identités et demander des signatures, et les octets de la clé privée ne quittent jamais l'hôte.ls ~/.ssh/id_*ne trouve rien. - AWS.
~/.aws/configpointe vers un assistant qui fournit le vrai identifiant de clé d'accès avec un faux secret de 40 caractères. Le Mac re-signe chaque requête avec le vrai secret en chemin vers AWS. - Kubernetes.
~/.kube/configest synthétique.kubectlparle au proxy du Mac, qui détient les vrais identifiants du cluster. - Jetons Docker, git et npm.
~/.docker/config.jsonet~/.git-credentialscontiennent des substituts qui ont la forme des vrais. Les jetons que vous importez, depuis un.envou à la main, deviennent des substituts limités à leurs hôtes. - Les jetons OAuth de Claude. Ils restent sur le Mac. L'agent tourne avec une clé factice, et le proxy place le vrai jeton d'accès sur le fil pour les requêtes vers Anthropic.
Les substituts sont aussi des pièges. Le proxy du Mac inspecte chaque requête
qui quitte la machine, en-têtes et corps, à la recherche d'un substitut envoyé
vers un hôte pour lequel il n'a pas été créé. Une balise qui emporte le mot de
passe brm-docker-… ou le faux jeton GitHub vers le serveur de l'attaquant
reçoit un HTTP 451. Rien n'est transmis, la VM se met en pause, et une alerte
intitulée This environment may have been compromised s'ouvre sur le Mac,
avec l'aperçu du jeton, le nom de l'identifiant, l'hôte auquel il appartient et
l'hôte vers lequel il a été envoyé. La détection apparaît comme une ligne rouge
dans le Security Timeline. Réglez Unmatched traffic sur Deny dans
le pare-feu Outbound connections de la machine, et une connexion vers un
hôte que le projet n'utilise pas est refusée avant tout cela.
Voilà qui répond à la remarque de Morin sur le niveau du runtime. Les contrôles se trouvent sur le Mac, sur le fil, hors de l'agent et hors de tout ce qui s'affiche dans la VM.
Le correctif et le prochain aperçu
Anthropic a corrigé SneakPeek en août et Noma l'a retesté. Six semaines ont séparé le signalement du correctif, et le premier patch est tombé face à un Web Worker le jour même de sa sortie. Les correctifs de moteur de rendu suivent souvent ce chemin. Un moteur de navigateur offre beaucoup de façons de lancer un script et beaucoup de façons de faire une requête, et chaque défense doit toutes les couvrir.
Les outils de développement ne cessent d'ajouter des volets qui affichent des choses : aperçus HTML, Markdown avec contenu intégré, sorties de notebooks, serveurs de développement, captures d'écran de l'application que l'agent vient de construire. Chacun est un moteur de rendu pointé sur du contenu que le développeur n'a pas écrit, issu de dépôts clonés une minute plus tôt. Sur un portable, chacun tourne à côté du dossier personnel.
Dans Bromure, le volet qui affiche les pages tourne dans une VM qui ne contient aucun de vos fichiers, et la machine où travaille l'agent ne contient que des substituts. Un bug de rendu y expose un Chromium jetable et des chaînes qu'aucun serveur n'acceptera.
Du code source, pas une page
L'agent lit le HTML comme du texte, et le volet Files le montre en source colorée. Ouvrir un rapport pour le relire n'exécute aucun de ses scripts.
Un navigateur au disque vide
Les pages s'affichent dans une VM Chromium séparée et jetable. Les fichiers de l'agent et ceux de votre Mac ne sont pas sur son disque, et elle oublie tout à la fermeture.
Un dossier personnel plein de substituts
Aucune clé privée SSH, un faux secret AWS, un kubeconfig synthétique, des substituts de jetons de registre et git. Les vrais identifiants restent chiffrés sur le Mac.
Une alarme sur le fil
Un substitut envoyé vers un hôte pour lequel il n'a pas été créé reçoit un 451. La VM se met en pause, une alerte nomme les deux hôtes, et le Security Timeline garde la trace.
Regardez le rapport
« Regarde le rapport de couverture » est à peu près la demande la plus banale qu'on puisse faire à un agent de code. Les développeurs continueront à l'interroger sur des fichiers de dépôts qu'ils n'ont pas écrits, et les outils continueront à afficher ces fichiers pour se rendre utiles.
Posez la question dans une machine Bromure, où la page s'affiche dans une VM qui n'a rien à lire et où les clés de l'agent sont des substituts. Pour en savoir plus sur les substituts et l'alarme qu'ils déclenchent, lisez L'agent a divulgué sa propre clé et La clé, c'était votre fichier .env.