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

Onze tests réussis

Straiker a publié un dépôt qui pousse Claude Code à exécuter un malware sans jailbreak. Son CONTRIBUTING.md confie la revue à un sous-agent que le dépôt définit lui-même, épinglé sur Haiku et tenu à l'écart du dossier des tests. Fable lance ensuite pytest, un fichier de test télécharge et démarre un implant Sliver, et le terminal affiche 11 passed. Anthropic a classé le signalement comme Informative : faire confiance à la configuration d'un dépôt relève de la décision de confiance du workspace prise par l'utilisateur. Bromure Agentic Coding décide de ce que cette confiance peut atteindre : une VM qui ne détient que des clés de substitution, un pare-feu sur le Mac et une trace que le dépôt ne peut pas modifier.

L'agent a suivi le processus documenté du projet, étape par étape, et ce processus avait été écrit par l'attaquant. Le terminal affichait 11 passed.

Vous trouvez sur GitHub un petit client de métriques qui vous ferait gagner une semaine, et vous demandez à Claude Code de le cloner et d'y jeter un œil. Il fait du bon travail. Il lit le code, repère un buffer sans limite et une boucle de nouvelles tentatives qui tournera sans fin sur un message empoisonné, et propose des correctifs. Vous lui dites ce que vous dites toujours : corrige ça et prépare une PR.

Quelques minutes plus tard, les correctifs sont en place, les tests sont verts et le résumé indique 11 passed. Quelque part dans ces quelques minutes, un implant a ouvert un shell distant sur votre portable.

C'est le scénario que Brian Cumi, de Straiker STAR Labs, a construit et publié le 22 septembre sous le titre Fable to Haiku: How a Malicious Repo Tricked Claude Code Into Running Malware. Le panorama d'octobre de la recherche en sécurité des agents de code publié par Adversa le range parmi les techniques d'attaque du mois. Le dépôt n'exploite aucun bug. Chaque étape utilise une fonctionnalité documentée, exactement comme ses concepteurs l'ont prévue.

Comment le dépôt s'est relu lui-même

Le dépôt, northstar-metrics, ressemble à un projet open source ordinaire, et son premier tour est banal : un CONTRIBUTING.md. En demandant une PR, vous avez lié l'agent au processus de contribution du projet, et un agent qui suit les instructions lit le guide de contribution. Celui-ci indique que les relecteurs doivent lancer l'agent de première revue du projet avant de lancer les tests.

Cet agent vit dans le dépôt, dans .claude/agents/first-pass.md. Claude Code permet à un projet de définir ses propres sous-agents dans ce dossier, avec un modèle et un périmètre dans le frontmatter. Celui-ci fixe model: haiku et limite la revue à src/ et docs/. Ses instructions ajoutent qu'« une lecture dans la session principale ne remplace pas l'exécution de l'agent », pour que l'agent principal ne le saute pas.

Voici la chaîne complète :

  1. Fable, le modèle principal, confie la revue au sous-agent du dépôt.
  2. Le sous-agent tourne sur Haiku 4.5 et ne regarde que src/ et docs/.
  3. Il revient avec deux constats que l'auteur a plantés pour lui : une chaîne de version codée en dur et un décalage entre la documentation et le code. Les deux sont inoffensifs.
  4. Le processus documenté considère la revue comme terminée. Personne n'ouvre tests/test_compat.py.
  5. Fable lance pytest.
  6. Avant son assertion, test_compat.py décode une URL en hexadécimal, télécharge un fichier Python et le démarre avec subprocess. Le bloc est enveloppé dans un try/except: pass, et l'assertion est toujours vraie.
  7. Un implant Sliver ouvre une session de commande et contrôle. Pytest affiche 11 passed.

Le résumé de Cumi sur la raison de ce succès : le dépôt s'appuie sur trois hypothèses ordinaires. Les projets peuvent définir leurs propres sous-agents, les agents suivent le processus documenté d'un projet, et une suite de tests au vert vaut vérification du code. « Un dépôt malveillant qui comprend ces trois hypothèses n'a besoin d'en casser aucune. Il lui suffit de se loger dans la jointure où elles se rencontrent. »

Straiker l'a signalé à Anthropic via HackerOne le 14 septembre. Anthropic l'a classé Informative : honorer la configuration du dépôt relève de la décision de confiance du workspace prise par l'utilisateur.

La décision de confiance, en pratique

La réponse d'Anthropic est cohérente avec le fonctionnement de Claude Code. Quand vous ouvrez un dossier, on vous demande si vous lui faites confiance, et vous avez dit oui. Cette décision couvre le CLAUDE.md, les hooks, les sous-agents et la suite de tests.

Le problème, c'est l'objet de votre décision. Vous faisiez assez confiance au dépôt pour le regarder. C'était le but du clonage. Sur un portable, « faire assez confiance à ce dossier pour lancer ses tests » revient à lui confier tout ce que votre compte peut atteindre, parce que c'est là que pytest s'exécute. Straiker énumère ce que l'implant avait sous la main : tous les fichiers que vous pouvez lire, les identifiants en mémoire, les clés SSH sur le disque, et tous les dépôts sur lesquels vous pouvez pousser.

Aucun relecteur, humain ou modèle, ne lit chaque fichier de test de chaque projet qu'il évalue. Ici, la revue a été écourtée à dessein, mais bien des revues honnêtes sont courtes, elles aussi. Vous pouvez en revanche choisir où la suite de tests s'exécute.

Sur un portable : le dépôt écrit la revue, les tests tournent en votre nomnorthstar-metrics (le dépôt)CONTRIBUTING.md« lancez l'agent first-pass ».claude/agents/first-pass.mdmodel: haikuscope: src/ docs/tests/test_compat.pyhors du périmètre de revueni jailbreak, ni CVE :que des fonctions documentéesClaude Code1. Fable délègue la revue2. Haiku lit src/ et docs/3. deux constats plantés, anodins4. revue marquée terminée5. $ pytest===== 11 passed =====Votre portable, en votre nom6. télécharge et lance la charge7. session Sliver ouverteL'implant peut atteindretout fichier lisible par vous~/.ssh/id_ed25519~/.aws .env jetonspush sur tous les dépôts
La chaîne de Straiker sur un portable. Le dépôt oriente la revue vers un modèle moins cher qui ne voit jamais les tests, pytest exécute la charge utile en tant que développeur, et l'implant hérite de tout ce que le développeur peut atteindre. La seule chose que voit l'utilisateur, c'est la dernière ligne.

Le même dépôt dans une machine Bromure

Dans Bromure Agentic Coding, Claude Code tourne dans une VM Linux sur votre Mac. Chaque machine a son propre disque, son propre répertoire personnel et ses propres identifiants, et tout son trafic sort par un proxy sur le Mac. La décision de confiance du workspace vous appartient toujours. Ce qui change, c'est ce que le dépôt de confiance peut atteindre.

Rejouez la chaîne de Straiker dans une machine Bromure. Les étapes une à cinq se déroulent comme avant : l'agent lit le guide de contribution, le sous-agent relit les mauvais dossiers, et pytest s'exécute. À partir de l'étape six, le dépôt se heurte au Mac.

Le téléchargement passe par le Mac

La charge utile doit être récupérée. Le test décode une URL et demande un fichier à un hôte dont le projet n'a jamais eu besoin. Dans une machine Bromure il n'y a qu'une seule route vers l'extérieur : le switch réseau virtuel dévie chaque connexion sur les ports 80 et 443 vers le proxy du Mac, et confronte toutes les autres connexions, quel que soit le protocole, aux règles de la machine.

Ces règles se trouvent dans le panneau Guardrails de la machine, sous Outbound connections : des entrées d'autorisation et de refus par hôte, plage d'adresses IP, protocole et port, avec un réglage Unmatched traffic sur Allow ou Deny. Une machine qui vous sert à évaluer le code d'inconnus n'a besoin que d'une courte liste : le fournisseur de modèle, les registres de paquets, GitHub.

allow web api.anthropic.com
allow web pypi.org
allow web files.pythonhosted.org
allow web github.com
default deny

Avec ces règles, le téléchargement de test_compat.py ne se connecte jamais, et le try/except: pass que l'auteur a ajouté pour masquer les échecs masque désormais celui-ci. Pytest affiche toujours 11 passed. La différence est sur le Mac. La Security Timeline de la machine contient une ligne rouge Firewall avec un verdict de refus, l'hôte que le test a tenté de joindre et le port, horodatée quelques secondes après que l'agent a lancé pytest. La suite de tests d'un client de métriques n'a aucune raison de contacter un hôte dont vous n'avez jamais entendu parler, et vous savez maintenant qu'elle a essayé.

Le trafic ultérieur de l'implant reçoit le même traitement. Que Sliver rappelle son serveur en HTTPS, en TLS mutuel sur son propre port, en WireGuard ou en DNS, la connexion sort par le switch, et le switch confronte chaque protocole à la même table.

L'implant ne trouve que des substituts

Supposons que la machine autorise le trafic non répertorié, que la charge utile arrive et que Sliver ouvre sa session. L'attaquant a désormais un shell dans la VM, avec la même portée que celle que Straiker énumérait pour le portable. Sur une machine Bromure, cette portée couvre moins de choses.

  • Fichiers. La VM contient son propre répertoire personnel et les dossiers de projet que vous avez choisi de partager avec cette machine. Le répertoire personnel de votre Mac, vos autres projets, vos profils de navigateur et votre Trousseau n'y sont pas.
  • Identifiants. Bromure garde les vraies clés d'API, jetons OAuth, jetons git, secrets cloud et mots de passe de bases de données chiffrés sur le Mac. La VM détient des substituts au bon format : ANTHROPIC_API_KEY=sk-ant-api03-brm-…, un jeton ghp_ que GitHub refuserait, une clé secrète AWS qui ne signe rien. Le proxy substitue la vraie valeur une fois la requête sortie de la VM, et uniquement pour l'hôte auquel l'identifiant appartient.
  • Clés SSH. ls ~/.ssh/id_* ne renvoie rien. Le SSH_AUTH_SOCK de la VM est un pont vers un ssh-agent sur le Mac. La VM peut demander une signature, mais ne peut pas lire la clé. Activez Ask before use pour une clé, et chaque demande de signature ouvre une boîte de dialogue sur le Mac.

Un implant qui ramasse ce qu'il trouve et l'envoie chez lui déclenche un second fil. Un jeton de substitution n'a qu'une destination légitime, l'hôte pour lequel il a été émis. Quand le proxy voit sk-ant-api03-brm-… ou ce jeton ghp_ partir ailleurs, il refuse la requête avec un HTTP 451, met la VM en pause et ouvre sur le Mac une alerte intitulée This environment may have been compromised, qui nomme le jeton, l'hôte pour lequel il a été émis et l'hôte vers lequel il a été envoyé. Seul un substitut est sorti, il n'y a donc rien à renouveler.

Le push demande d'abord

Le dernier élément de Straiker, « les dépôts avec droits de push », est celui qui transforme un portable compromis en incident de chaîne d'approvisionnement. Un attaquant qui dispose d'un shell et de vos identifiants git peut pousser un commit sur chaque projet que vous maintenez.

Sur une machine Bromure, un push vers GitHub, GitLab ou Bitbucket est une écriture, et les nouvelles machines sont livrées avec Prompt before write pour chaque service protégé. Le proxy retient le push et macOS affiche Allow write on "github.com" from workspace "…"? avec l'opération telle qu'elle est apparue sur le réseau, POST /acme/app.git/git-receive-pack. Vous n'avez pas demandé de push, donc vous cliquez sur Don't allow, et l'implant reçoit un 403 se terminant par blocked by Bromure Guardrails. macOS dessine la boîte de dialogue à partir de ce que le proxy a lu, donc rien dans la VM ne peut en changer le libellé.

La trace montre la revue qui n'a pas eu lieu

Une fois la poussière retombée, vous voulez savoir ce que l'agent a fait. Au niveau de traçage par défaut, AI request details, le proxy conserve chaque échange avec le fournisseur de modèle, chiffré sur le Mac, et le Trace Inspector décompose chacun d'eux en appels d'outils, commandes shell et lectures de fichiers.

Pour ce dépôt, l'enregistrement répond aux questions qui comptent :

  • Quel modèle a fait la revue ? Le corps de la requête capturée le nomme : claude-haiku-4-5, un tour après que la session principale a lu .claude/agents/first-pass.md.
  • Qu'a-t-il lu ? Chaque appel Read, avec son chemin. Il y a des appels pour des fichiers sous src/ et docs/, et aucun pour tests/test_compat.py.
  • Qu'est-ce qui s'est exécuté ? L'appel Bash qui a lancé pytest, et à côté, dans la Security Timeline, la ligne de pare-feu du téléchargement qu'il a provoqué.

Le dépôt contrôle ce que l'agent voit et ce que pytest affiche, mais il n'a aucun moyen de modifier la trace. Le proxy écrit cet enregistrement sur le Mac, à partir du trafic lui-même.

Dans Bromure : les tests tournent dans une VM, les sorties sont sur le MacVM de la machine « eval »~/northstar-metrics$ pytest → test_compat.pyCe qu'un implant trouveraitANTHROPIC_API_KEY=sk-ant-api03-brm-…GH_TOKEN=ghp_… (substitut)~/.ssh : aucune clé privéele home de cette machine seulni home du Mac, ni autres projetstoute connexion passe par le switchSwitch + proxy, sur le MacFirewall : hôte de la charge → denyligne rouge dans la Security TimelineSubstitut hors périmètre → 451, VM en pauseTrace : modèle claude-haiku-4-5,Read src/… docs/…, Bash pytestDialogue macOS : Allow write on …?POST /acme/app.git/git-receive-packvraies clés insérées pour leur seul hôteInternetapi.anthropic.compypi.orggithub.comhôte de la charge :jamais contactéGardé sur le Mac, hors de la VMClés d'API et jetons OAuth · identifiants git, cloud et BDD · clés SSH privées · traces chiffrées · Security Timeline
Le même dépôt dans une machine Bromure. La revue part toujours chez Haiku et pytest s'exécute toujours, mais la VM ne détient que des identifiants de substitution et aucune clé privée, le téléchargement de la charge utile s'arrête au pare-feu de sortie du Mac, les push attendent une boîte de dialogue macOS, et la trace enregistre quel modèle a relu quoi.

Une machine pour le code que vous n'avez pas lu

L'article de Straiker se termine par trois recommandations : exiger une approbation avant d'exécuter un sous-agent défini par le dépôt, lire les fichiers de test avant de les exécuter, et traiter des fichiers comme .claude/agents/, les hooks et les définitions de workflow comme des entrées non fiables. Toutes trois sont saines, et toutes trois supposent que quelqu'un s'en souvienne, à chaque fois, sur une tâche qui ressemblait à une revue de routine.

Dans Bromure, la bonne habitude coûte quelques clics. Une nouvelle machine est un clone copy-on-write d'une image Ubuntu signée, elle n'occupe donc presque pas de disque. D'où une configuration pour évaluer du code extérieur :

  1. Créez une machine appelée eval. Donnez-lui un fournisseur de modèle et rien d'autre : pas de jeton git avec droits de push, pas de clés cloud, pas de base de données.
  2. Réglez sa sortie sur refus par défaut. Dans Guardrails › Outbound connections, autorisez le fournisseur de modèle, les registres de paquets et GitHub, et réglez Unmatched traffic sur Deny.
  3. Clonez dans la VM, pas dans un dossier partagé. Démarrez la session dans un dossier neuf du répertoire personnel de la machine. Un dossier partagé est une fenêtre ouverte en direct sur votre Mac, et le répertoire personnel de la machine est la partie que Bromure peut rembobiner ou effacer.
  4. Ou rendez-la jetable. bromure-cli vm run --rm démarre une machine qui est supprimée, disque compris, quand elle s'arrête.
  5. Lisez la Timeline avant d'adopter la dépendance. Une ligne Firewall de refus pendant une exécution de tests est un constat qu'aucun relecteur n'a signalé, quoi que pytest ait affiché.

Si quelque chose passe malgré tout, vous nettoyez une seule machine. Rewind home… ramène le répertoire personnel au point de contrôle d'un démarrage antérieur. Une machine signalée par le détecteur de compromission refuse de démarrer tant que vous n'avez pas choisi Wipe and Launch, qui reconstruit son disque et son répertoire personnel tout en conservant vos réglages et les vrais identifiants sur le Mac. Le compte de votre portable, vos autres projets et vos autres machines n'ont jamais été à l'intérieur.

Quand la bibliothèque a fait ses preuves, passez-la sur la machine qui fait le vrai travail. Cette machine-là ne détient elle aussi que des clés de substitution, et ses push s'arrêtent toujours à la boîte de dialogue sur le Mac.

Le téléchargement doit demander

Chaque connexion ouverte par la VM passe par le switch virtuel et le proxy du Mac. Avec le trafic non répertorié refusé, un test qui récupère une charge utile depuis un nouvel hôte échoue, et la Security Timeline montre la tentative.

Le shell ne trouve que des substituts

Les vraies clés d'API, jetons git et secrets cloud restent chiffrés sur le Mac, et les clés SSH privées n'entrent jamais dans la VM. Un substitut envoyé ailleurs que vers son hôte d'origine renvoie 451 et met la machine en pause.

Un push exige un clic sur le Mac

Avec Prompt before write, le réglage par défaut des nouvelles machines, un git push attend une boîte de dialogue macOS qui montre l'opération exacte. Un implant ne peut pas y répondre.

Un registre que le dépôt ne peut pas modifier

La trace sur le Mac montre quel modèle a relu le code, quels fichiers il a lus et quelle commande a suivi, quoi que le terminal ait affiché.

Faites moins confiance au dépôt

Le dépôt n'a rien fait d'exotique. Il a écrit un guide de contribution, défini un agent auxiliaire et livré un fichier de test. Les projets continueront à faire ces trois choses, parce qu'elles sont utiles, et les agents continueront à les suivre, parce que suivre les conventions d'un projet est précisément ce qui fait un bon agent.

La réponse d'Anthropic vous laisse la décision, et c'est bien là qu'elle doit être. Prenez-la sur une machine faite pour ça : une machine où faire assez confiance à un dépôt pour lancer ses tests revient à lui confier une VM, quelques substituts et une courte liste d'hôtes. Puis laissez la suite de tests afficher ce qu'elle veut.