Il suffisait d'être connecté
Un développeur qui faisait de la place sur son disque a trouvé 700 Mo dans un répertoire caché et a tiré sur le fil : ZCode, de Z.ai, empaquetait dans un tar tout son arbre de travail, 42 411 fichiers dont 86,6 % de .git, le chiffrait avec une clé que seul Z.ai détient, et le postait directement dans un bucket Alibaba Cloud. Aucun attaquant, aucune CVE, aucun malware. Le déclencheur, c'était d'être connecté. Le client chiffrait l'archive, donc aucun scanner ne pouvait la lire, et les seuls faits qui restaient étaient la destination et la taille. Ce sont deux des champs que Bromure Agentic Coding note pour chaque requête émise par un workspace, sur un fil que l'outil à l'intérieur de la VM ne choisit pas.
Votre outil de codage n'a pas besoin d'être compromis pour envoyer votre code source quelque part. Il lui suffit d'un réglage par défaut. Faire confiance à l'éditeur n'est pas la question. Demandez-vous si vous le sauriez.
Vous faites de la place sur un disque plein. Un répertoire caché dans votre
dossier personnel occupe plus de 700 mégaoctets, et vous ne vous souvenez pas
d'avoir installé quoi que ce soit de cette taille. À l'intérieur, sous un dossier
nommé checkpoints, il y a un unique fichier de 313 Mo avec une extension .enc
et, à côté, une fiche de métadonnées qui indique failureCount: 564.
Cinq cent soixante-quatre tentatives de quelque chose que vous n'avez jamais demandé.
Sept cents mégaoctets et un fil à tirer
C'est là que le développeur chinois connu sous le nom de ferstar a commencé le 18 septembre, et le compte rendu de reverse engineering de ce qu'il a trouvé vaut d'être lu en entier. Le répertoire appartenait à ZCode, le client de codage IA de bureau de Z.ai, l'entreprise derrière les modèles GLM. CSO Online et InfoWorld reprennent tous deux son résumé du comportement mot pour mot :
« Dès que vous êtes connecté, ZCode empaquette silencieusement l'intégralité de votre workspace — historique .git complet, cache d'assets LFS, reflogs et configurations globales de l'application —, le chiffre, et le téléverse directement vers Aliyun OSS. »
Le déclencheur est dans la première proposition. Pas une question que vous avez posée, pas la fonction d'indexation que vous avez activée : un jeton de session valide, que le processus de capture attend, et rien d'autre. Une seule session a enregistré 62 événements de capture dans ses journaux. Quand il a supprimé l'archive, le client en a reconstruit une en moins d'une demi-heure et a incrémenté le compteur d'échecs.
Ce qu'il y avait dans la boîte
L'instantané qu'il a démonté contenait 42 411 fichiers, 345 Mo avant chiffrement et 313 Mo après. Ce qui composait ces 345 Mo :
.git/lfs/: 196,1 Mo, 56,8 %.git/objects/: 102,2 Mo, 29,6 %.git/logs/: 0,6 Mo, 0,2 %- tout le reste, c'est-à-dire le code et la documentation réels : 46,2 Mo, 13,4 %
86,6 % du téléversement était du .git. L'archive emportait chaque objet de
l'historique du dépôt, chaque gros binaire que le cache LFS avait récupéré, et le
reflog, qui liste chaque branche sur laquelle vous êtes passé et chaque commit
que vous avez fait puis annulé. Qui lit votre arbre de travail apprend ce que
vous livrez ; qui lit votre .git apprend ce que vous avez essayé, ce que vous
avez annulé, et qui l'a committé.
Les deux interrupteurs gouvernaient leur côté
ZCode était livré avec deux réglages qu'un développeur soigneux aurait trouvés et désactivés : un pour l'optimisation de l'expérience, un pour l'indexation des instantanés de dépôt. Tous deux faisaient quelque chose de réel, et aucun ne faisait ce que son nom suggère.
Le premier gouvernait si Z.ai était autorisé à utiliser les données. Le second gouvernait si le serveur indexait l'instantané une fois arrivé. L'empaquetage et le téléversement tournaient quoi qu'il arrive, parce que le side-car de capture démarre dès que le fournisseur de jetons lui remet un JWT valide, sans aucune préférence utilisateur dans le chemin. Désactivez les deux interrupteurs et le client empaquetait toujours votre dépôt, le chiffrait toujours, le postait toujours dans le bucket, et promettait de la discrétion sur ce qui se passait ensuite.
Le bug d'un seul éditeur, mais la forme se répète partout. Un contrôle qui vit à l'intérieur du programme qu'il contraint est une requête. Il tourne sur le code même qui devrait être fautif pour que vous en ayez besoin.
Rien sur le fil ne pouvait le lire
Le client chiffrait chaque archive en AES-256-CTR avant qu'elle ne quitte la
machine et enveloppait la clé symétrique avec une clé publique RSA-OAEP-SHA256
que lui remettait zcode.z.ai. La clé privée correspondante ne descend jamais.
ferstar ne pouvait pas ouvrir sa propre archive ; la seule partie qui pouvait
était celle qui détenait le bucket.
Cela écarte toutes les défenses que vous possédez et qui fonctionnent en lisant des octets. Un scanner de secrets ne trouve aucune clé AWS dans ce tarball, et l'inspection profonde de paquets voit une session TLS vers un point de terminaison de stockage transportant du chiffré enveloppé dans du chiffré.
Deux faits survivent : où c'est allé et quelle taille ça faisait. Un POST de 313 Mo vers un hôte de stockage objet dans une région où votre équipe ne déploie pas se remarque, à condition que quelqu'un note où vos outils se connectent et combien ils envoient. Personne ne le faisait, et c'est pourquoi trouver cela a exigé de l'archéologie d'espace disque et une semaine de reverse engineering au lieu d'un coup d'œil à un journal.
Où se trouve la sortie
Bromure Agentic Coding place l'agent et sa chaîne d'outils dans une VM Ubuntu sur votre Mac, et ne donne à cette VM qu'une seule façon d'atteindre Internet : un proxy man-in-the-middle qui tourne sur l'hôte, en dehors de l'invité, là où aucun processus de la VM ne peut le reconfigurer.
Cela ne demande rien au logiciel. Le commutateur réseau virtuel détourne de
façon transparente les flux de la VM sur les ports 80 et 443 vers le proxy de
l'hôte, sans aucune variable d'environnement requise et sans rien que l'invité
puisse désactiver, actif par défaut pour chaque workspace. Un outil qui
ignore HTTPS_PROXY, ouvre sa propre socket et parle directement à un point de
terminaison de stockage passe quand même par là, parce que l'interception vit
dans le commutateur où l'invité est branché plutôt que dans l'invité.
Quatre choses arrivent alors à un POST de 313 Mo que personne n'a demandé.
Chaque requête laisse une trace
Chaque requête qui passe par le proxy laisse un enregistrement de trace : horodatage, hôte, port, méthode, chemin, statut, latence, et octets de requête mesurés avant tout échange d'identifiants. Vous n'avez pas besoin de déchiffrer une archive pour remarquer un téléversement de la taille d'un dépôt vers un hôte où vous n'avez jamais déployé : la taille et la destination sont des colonnes. Triez sur l'une, filtrez sur l'autre.
Ça ne quitte jamais le Mac
Le pare-feu de sortie par workspace est une table de règles ordonnée
(action, protocole, hôte ou CIDR, ports, méthodes HTTP) qui se termine par
default allow ou default deny. Faites tourner un workspace en
mode liste blanche et le téléversement n'a nulle part où aller. Deux endroits
appliquent les règles : le commutateur compare l'IP de destination et le nom
d'hôte capté par DNS, le proxy compare à nouveau le nom de serveur TLS, et
vos modifications atteignent les sessions en cours immédiatement.
Chaque verdict est une ligne
Chaque décision du pare-feu arrive dans la Security Timeline, donc un outil qui n'arrête pas de réessayer, 564 fois par exemple, apparaît comme un refus répété contre un hôte nommé plutôt que comme un disque qui se remplit lentement. Le motif qu'il a fallu une semaine à reconstituer est précisément ce que cette fenêtre est faite pour afficher.
Les configs qu'il emporte sont des leurres
« Configurations globales de l'application » est l'expression qui devrait
vous inquiéter le plus dans la phrase de ferstar. Dans un workspace
Bromure, ~/.git-credentials, ~/.docker/config.json, ~/.kube/config et
~/.aws/config existent et fonctionnent, et chaque valeur qu'ils
contiennent est un faux qui préserve la structure. Aucun fichier, variable
d'environnement ou processus de la VM ne détient jamais une vraie clé d'API,
un vrai jeton OAuth, un vrai secret AWS ou une vraie clé privée SSH.
La portée fait le reste. Un workspace, c'est un projet, ou une frontière
d'identifiants ; deux workspaces ne partagent jamais une VM, et le
dossier personnel de l'invité est son propre fichier image. Ce qu'un outil
empaquette dans un workspace, c'est ce workspace, ce qui laisse
de côté les quarante autres dépôts de votre Mac, le ~/.aws/config de votre
hôte, et vos clés SSH. Les clés n'atteignent jamais un invité : la signature se
fait sur l'hôte via un pont vsock, et les octets privés y restent.
Ce que Z.ai a fait ensuite
Z.ai s'est excusé le jour où ferstar a publié. L'entreprise a désactivé le flux de téléversement dans ZCode v3.14.0, dont InfoWorld rapporte qu'il est sorti le 19 septembre en mentionnant un correctif pour un problème anormal de téléversement de dépôt, et a fait appel à la société de sécurité NSFOCUS pour auditer le résultat. NSFOCUS a confirmé que « tous les objets de données du bucket Alibaba Cloud OSS zcode-prod, ainsi que le bucket lui-même, ont été supprimés », et Z.ai dit qu'après l'audit « aucun chemin fonctionnel capable de déclencher la génération d'instantanés locaux de dépôt ou de transmettre des fichiers locaux vers l'extérieur n'a été identifié », et que le code « n'a jamais servi à l'entraînement de modèles ». Puis ils en ont ouvert le code : client, backend, UI partagée, Agent CLI et runtime, sous Apache-2.0, sur GitHub. Cela vaut mieux que la norme du secteur, et cela signifie que la prochaine personne qui voudra vérifier n'aura pas besoin d'un désassembleur.
Cris Thomas, de Semgrep, cité dans la couverture de CSO, a nommé la catégorie : « Ce n'est pas vraiment un problème de modèle d'IA, c'est un problème d'architecture de sécurité à l'ancienne. » Il veut que les éditeurs divulguent ce qui quitte la machine, où cela va et combien de temps c'est conservé, « avec les permissions minimales activées par défaut, pas les maximales ». Aucun client ne peut imposer cela depuis l'extérieur.
Le réglage par défaut est le modèle de menace
Personne n'a attaqué personne ici. Personne n'a volé d'identifiant, empoisonné de paquet ou convaincu un modèle de quoi que ce soit. Un produit a fait ce que ses auteurs l'avaient construit pour faire, et ce qu'ils l'avaient construit pour faire était d'expédier votre dépôt quelque part où vous ne pouviez pas le lire. L'incident est un réglage par défaut, et ce sont des gens dont les intérêts sont voisins des vôtres qui choisissent les réglages par défaut.
C'est l'argument pour placer la frontière en dehors de l'outil. Vous n'avez pas à croire que votre assistant de codage est hostile ; vous avez seulement à remarquer que vous ne pouvez pas auditer ce que vous ne voyez pas, et que le panneau de réglages de l'éditeur est un endroit étrange pour ranger l'interrupteur. Faites tourner l'agent et sa chaîne d'outils là où chaque connexion qu'ils ouvrent traverse un fil qui vous appartient, où la trace de ce passage est écrite sur votre Mac, et où la règle qui arrête un téléversement de 313 Mo est une ligne que vous avez tapée plutôt qu'une case cochée que quelqu'un d'autre a honorée.
Alors « qu'est-ce que mon outil de codage envoie ? » est une fenêtre que vous ouvrez un mardi après-midi, pas un projet de recherche.