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

Le dépôt était bel et bien celui de Microsoft

Les 5 et 6 juin 2026, le ver Miasma a poussé du code voleur de credentials dans 73 dépôts répartis sur quatre des organisations GitHub propres à Microsoft — Azure, Azure-Samples, microsoft, MicrosoftDocs — dont Azure/functions-action, l'Action de déploiement officielle, et durabletask, un dépôt qui avait déjà été nettoyé une fois en mai. Cette fois, la charge utile n'a pas attendu npm install. Elle s'est déclenchée à l'instant où un développeur ouvrait le dépôt dans Claude Code, Cursor, Gemini CLI ou VS Code. Voici pourquoi le signal de confiance — « c'est un dépôt Microsoft » — était de nouveau la surface d'attaque, et ce qui change quand l'agent qui l'ouvre vit dans une VM Bromure par profil, derrière un courtier de credentials, un guardrail lecture/écriture et un cooldown de packages.

Il y a une semaine, le ver est entré par le scope npm de Red Hat et s'est déclenché via un hook preinstall. Cette semaine, il est entré par le GitHub de Microsoft et n'a eu besoin d'aucune installation. Miasma a planté de la configuration à portée projet dans 73 dépôts appartenant à Microsoft — Azure/functions-action, l'Action de déploiement officielle, parmi eux — et la charge utile s'est exécutée à l'instant où un développeur ouvrait le dépôt dans Claude Code, Cursor, Gemini CLI ou VS Code. Le clonage n'était pas le déclencheur. L'ouverture dans votre agent l'était.

Un développeur qui clone Azure/functions-action pour déboguer un déploiement qui échoue n'hésite pas, et l'agent qu'il y pointe non plus. C'est un dépôt de première partie issu de l'organisation Azure propre à Microsoft — la source canonique de l'Action GitHub que la moitié de l'écosystème référence comme Azure/functions-action@v1. Il n'y a aucun mainteneur à vérifier, parce que le mainteneur est Microsoft. Il n'y a aucun nom à scruter, parce que le nom est épelé exactement comme il devrait l'être. Donc le dépôt est ouvert, et l'agent lit la configuration du projet comme le fait tout outil de codage moderne à l'ouverture d'un dossier — et l'un de ces fichiers de config pointe vers une commande, et la commande est un blob d'environ 4,3 mégaoctets qui se met à lire le système de fichiers à la recherche de clés.

Nous avons décrit la moitié npm de cette campagne exacte il y a sept jours. Miasma est le même ver — une variante du code « Mini Shai-Hulud » que TeamPCP a publié au grand jour à la mi-mai — et le fait inconfortable qu'il démontre est le même, vissé d'un cran de plus : source réputée n'est pas un contrôle de sécurité. On a l'impression que ç'en est un. La plupart des conseils de supply-chain s'appuient dessus. Et le 5 juin, ça n'a strictement rien acheté, doublement : l'espace de noms était celui de Microsoft, et l'exécution n'est pas venue d'un package que vous avez choisi d'installer. Elle est venue de l'ouverture d'un dossier.

Ce que Miasma a fait aux dépôts de Microsoft.

Les 5 et 6 juin 2026, GitHub a désactivé 73 dépôts répartis sur quatre organisations GitHub de Microsoft après que des commits malveillants y aient été poussés, selon The Hacker News et une analyse détaillée de StepSecurity. Redmond Magazine l'a couvert le 8 juin. La répartition :

  • Azure — 49 dépôts, dont Azure/functions-action (l'Action de déploiement officielle de Functions) et les language workers pour .NET, Python, Java, Go et PowerShell.
  • microsoft — 10 dépôts.
  • Azure-Samples — 13 dépôts.
  • MicrosoftDocs — 1 dépôt.

La forme, réduite à sa mécanique :

  • Le point d'entrée était un compte de contributeur précédemment compromis disposant d'un accès en écriture, utilisé pour pousser des commits malveillants directement dans les dépôts — taggés [skip ci] de sorte que les changements se glissaient devant les contrôles CI/CD qui auraient autrement tourné.
  • Le commit plantait de la configuration à portée projet — le genre de fichier qu'un agent de codage ou un IDE lit et exécute automatiquement quand vous ouvrez le dossier : une tâche d'éditeur, un hook d'agent, un serveur MCP défini par le projet. C'est la même classe de frontière de confiance que TrustFall d'Adversa AI a démontrée à travers Claude Code, Cursor CLI, Gemini CLI et Copilot CLI — les quatre exécutent la configuration définie par le projet juste après l'invite de confiance du dossier.
  • La charge utile — environ 4,3 Mo de code obfusqué — s'exécutait quand le dépôt était ouvert dans Claude Code, Gemini CLI, Cursor ou VS Code, ou lancé via un script npm test. Pas au clonage seul. C'est le fait de pointer votre agent vers l'arborescence clonée qui l'exécutait.
  • À l'exécution, elle balayait l'hôte à la recherche de tokens GitHub, clés AWS, service principals Azure, credentials GCP, tokens de publication npm et PyPI, clés SSH et fichiers .env, puis utilisait l'accès volé pour se commiter plus loin — ce qui en fait un ver plutôt qu'un coup unique.

Un détail vaut qu'on s'y arrête : Azure/durabletask figurait parmi les dépôts touchés — et il avait déjà été compromis en mai dans la campagne TeamPCP, puis nettoyé. Un dépôt qui avait été remédié une fois a été ré-empoisonné cinq semaines plus tard. Le nettoyage n'est pas un état que l'on atteint et conserve ; c'est un état d'où l'on retombe à l'instant où un autre credential de la chaîne est dérobé.

Il vaut la peine d'être tout aussi précis sur ce qui n'a pas eu lieu. Le réseau corporate de Microsoft n'a pas été percé. Azure, le service cloud, n'a pas été percé. Aucune donnée client ni aucun système de production n'a été touché. C'était une attaque sur des dépôts de code source — et sa conséquence la plus largement ressentie n'avait rien à voir avec le malware lui-même : à l'instant où GitHub a désactivé Azure/functions-action, chaque pipeline sur terre qui référençait Azure/functions-action@v1 a cessé de se résoudre. Microsoft était le porteur le plus en vue. Les gens réellement compromis étaient les développeurs qui ont ouvert les dépôts empoisonnés dans un agent entre le 3 et le 5 juin, et se sont fait balayer leurs credentials de leurs propres machines.

LAPTOP DÉVELOPPEUR — secrets de l'hôte visibles par ce que l'agent exécute à l'ouvertureGITHUB PROPRE À MICROSOFTcompte contributeurcompromis → push [skip ci]Azure/functions-actionAzure/durabletask (encore)+ config projet plantéeLE DÉVELOPPEUR CLONEgit clone …/functions-actionrien ne s'exécute encoredépôt de 1re partie ⇒aucune raison d'hésiterOUVERT DANS L'AGENT DE CODAGE — la charge se déclencheClaude Code · Cursor · Gemini CLI · VS Codeouverture dossier → lit la config projethook d'agent / tâche / serveur MCP↳ exécute la charge utile 4,3 Mo(le clone seul ne l'a PAS exécutée)SYSTÈME DE FICHIERS & ENV DE L'HÔTE — tout réel, tout lisible par la charge$GITHUB_TOKEN, gh hosts.ymlaccès push (réel)~/.aws, service principals Azureclés cloud (réelles)~/.npmrc, token PyPIpublier ici (réel)~/.ssh/id_ed25519, .envclés + secrets CI (réels)tar | chiffrer | exfiltrer→ récoltés de votre machineAUTO-PROPAGATIONaccès GitHub volécommiter dans le dépôt suivantplanter la même config73 dépôts, 4 orgsdurabletask : touché deux fois
Miasma sur une machine de développeur qui ouvre un dépôt Microsoft affecté dans un agent de codage. Un compte de contributeur précédemment compromis pousse un commit [skip ci] qui plante de la configuration à portée projet — un hook d'agent, une tâche d'éditeur, une définition de serveur MCP. Le développeur clone le dépôt (rien ne s'exécute) et l'ouvre dans Claude Code, Cursor, Gemini CLI ou VS Code. À l'ouverture du dossier, l'agent lit et exécute cette configuration, lançant une charge utile de 4,3 Mo qui balaie l'hôte à la recherche de tokens GitHub, clés AWS, service principals Azure, tokens npm/PyPI, clés SSH et fichiers .env, les chiffre, les expédie, et utilise l'accès GitHub volé pour se commiter dans le dépôt suivant. Pas de typosquat, pas de faux mainteneur, pas de npm install. Le signal de confiance est le nom de l'organisation Microsoft, et l'exécution vient de l'ouverture d'un dossier.

Le même dépôt, ouvert à l'intérieur de Bromure Agentic Coding.

Bromure Agentic Coding fait tourner votre agent de codage à l'intérieur d'une VM Linux par profil — son propre noyau, son propre système de fichiers, sa propre pile réseau, sur le framework Virtualization d'Apple. Un profil est un périmètre de travail cohérent : ce client, ce service interne, ce dépôt open-source que vous avez cloné pour déboguer un déploiement. Vous clonez Azure/functions-action dans ce profil et vous l'ouvrez avec votre agent à l'intérieur. Le déclencheur d'ouverture de dossier se déclenche exactement comme prévu. La charge utile s'exécute. Et ensuite elle part chercher des clés sur un hôte qu'elle ne peut pas atteindre.

Parce que les credentials ne sont pas dans le profil. La VM est livrée avec des stubs — de faux tokens qui paraissent réels à git, gh, aws, kubectl, npm, et à tout ce qui attend un en-tête Authorization. Un proxy sur votre Mac se tient devant chaque connexion qui quitte le sandbox, reconnaît le stub, et l'échange contre le vrai secret au fil au moment où la requête part (le sandbox qui détenait la clé parcourt le mécanisme). Le vrai PAT GitHub, la vraie clé AWS, le vrai principal Azure — aucun d'eux ne touche un fichier, une variable d'environnement ou une page de mémoire que la VM peut lire. Les clés SSH ne quittent jamais le Keychain de macOS du tout ; seul le socket ssh-agent est forwardé à l'intérieur, comme OpenSSH l'a toujours voulu.

Alors parcourons le balayage de Miasma à travers cette frontière. La charge utile lit l'environnement à la recherche de $GITHUB_TOKEN et trouve un stub. Elle lit ~/.aws et ne trouve rien. Elle lit ~/.npmrc et ne trouve aucun token de publication. Elle lit ~/.ssh et ne trouve aucun fichier de clé — il y a un socket forwardé, pas une clé privée sur disque. Le blob de 4,3 Mo s'exécute jusqu'au bout exactement comme écrit. Il exfiltre juste une boîte qui n'a jamais détenu vos clés, du mauvais côté d'une frontière imposée par le matériel par rapport à tout ce qui compte.

VM BROMURE PAR PROFILl'agent ouvre functions-actionconfig projet → la charge se déclenches'exécute, mais dans l'invité seulCE QUE LA CHARGE VOIT$GITHUB_TOKENstub_7f3a…~/.aws, ~/.npmrcstub / absentfichier de clé ~/.sshsocket seulpropager : git push (soi-même)besoin d'écrire → demande au proxyexfil clés hôte : rien à prendrebesoin d'une dépendance ?npm install → la récup quitte la VMà travers le proxy de BromurePROXY · VOTRE MACCOURTIER DE CREDENTIALSvrais PAT / clés détenus icistub → réel, au filla valeur n'entre jamais dans la VMGUARDRAIL : LECTURE/ÉCRITUREpush = muter → INVITEnomme le verbe + la cibleSUPPLY CHAINscan OSV + socket.devcooldown : < 2 jours retenuscripts d'install retirésRÉSULTATclés hôte :stubs récoltés,les vraies intactespush pour propager :en pause, vous refusezpackage frais malveillant :n'atteint jamais la VMrayon d'explosion = 1 profil
Trois couches, dans l'ordre où Miasma les rencontre. (1) Supply-chain : le proxy scanne chaque récupération de package contre OSV et socket.dev et met en quarantaine les versions plus jeunes que le cooldown — de sorte que l'agent ne peut même pas tirer une dépendance malveillante fraîche tant que l'écosystème rattrape encore son retard. (2) Courtage de credentials : la VM ne détient que des stubs ; les vrais secrets se trouvent sur l'hôte derrière le proxy, échangés au fil, de sorte que le balayage de l'hôte par la charge ne trouve que des marque-places. (3) Guardrails : l'étape de propagation du ver — un git push pour se commiter plus loin — est un appel qui change l'état, et un guardrail lecture/écriture l'arrête au fil et demande, en nommant le verbe et la cible. Chaque couche est imposée sous l'agent, à la frontière de la VM que l'agent ne peut pas contourner.

L'étape de propagation est une écriture, et les écritures reçoivent une invite.

Voler des clés n'est que la moitié de Miasma. L'autre moitié est la propagation : il a utilisé l'accès GitHub volé pour se commiter dans le dépôt suivant, et c'est ce qui a transformé une poignée de dépôts empoisonnés en 73. Même dans un profil qui dispose légitimement d'un accès push — disons que vous avez cloné functions-action précisément parce que vous comptez ouvrir une PR contre lui — l'étape de propagation du ver doit quand même sortir par le proxy, et c'est là que les Guardrails la rencontrent.

Les Guardrails lisent l'opération, pas seulement la connexion — ils distinguent une lecture d'une écriture. Un git fetch est une lecture ; un git push est une écriture. Réglez le credential GitHub d'un profil sur demander à l'écriture, et à l'instant où l'agent atteint un appel qui change l'état — le git-receive-pack dont le ver a besoin pour recommiter sa config, un DELETE contre une API, un Terminate* sur EC2 — Bromure l'arrête au fil et fait apparaître une invite sur votre Mac qui nomme le verbe, la cible et le profil. L'octroi que vous donnez est borné dans le temps : quinze minutes pour une publication, à usage unique pour les plus effrayants, jamais si la demande n'a aucun sens. Les lectures ne vous interrompent jamais ; l'agent fetch, grep et lit toute la journée. C'est la mutation qui met en pause.

C'est la différence entre « l'agent a un token » et « l'agent peut faire ce qu'il veut avec le token ». Tout le mécanisme de propagation de Miasma est une écriture que l'agent ne vous a jamais dit qu'il faisait — et une écriture que l'agent ne vous a jamais dit qu'il faisait est exactement ce que l'invite lecture/écriture est conçue pour attraper. Le push qui propage le ver devient une boîte de dialogue sur laquelle vous cliquez Refuser, de la même manière que « l'agent a supprimé la base de données de production » cesse d'être un post-mortem et devient une invite que vous avez déclinée.

La version avait quelques heures, et Bromure fait vieillir les packages.

Il existe une seconde voie par laquelle Miasma — et la lignée plus large Mini Shai-Hulud — atteint un développeur : non pas par un dépôt que vous ouvrez, mais par un package fraîchement empoisonné que l'agent installe en faisant son travail. La moitié Red Hat de cette campagne était précisément cela, un hook preinstall sur 32 packages dans un scope de confiance. Et le détail brutal de ces incidents est le timing : une version compromise se fait typiquement attraper et retirer en quelques heures — mais ce sont exactement les heures durant lesquelles un agent autonome, tournant sans surveillance, pourrait la tirer.

La couche Supply Chain de Bromure transforme la même frontière proxy en point de contrôle de scan, et elle fait les deux choses qui comptent réellement contre une compromission le jour même :

  • Elle force le scan de chaque récupération contre socket.dev en plus d'OSV. OSV attrape les CVE connues au-dessus du seuil de sévérité que vous fixez. socket.dev attrape ce que les bases de données de vulnérabilités n'ont pas encore rattrapé — scripts d'installation malveillants, malware comportemental, typosquats, la compromission tout juste publiée. Une version signalée est bloquée avant que le tarball n'atterrisse dans la VM. De façon cruciale, le scan tourne sous l'agent, au niveau du proxy : quelle que soit la façon dont l'agent réécrit sa propre config pour vous contourner, la récupération quitte quand même par la frontière qu'il ne peut pas franchir.
  • Elle impose un cooldown. Bromure met en quarantaine toute version publiée au cours des deux derniers jours — ajustable — de sorte qu'une version uploadée il y a une heure est simplement non installable dans ce profil le temps que l'écosystème rattrape son retard. Contre un ver dont toute la fenêtre d'opportunité est l'écart entre publier et retirer, un cooldown n'est pas une heuristique sur l'apparence malveillante d'un package. C'est un refus d'être le premier à le découvrir. Combinez-le avec le retrait des scripts d'installation que Bromure fait à la volée — en extrayant les hooks postinstall du tarball et en corrigeant le hash des métadonnées de sorte que l'installation se vérifie toujours — et le package qui atterrit atterrit inerte.

Pour Miasma spécifiquement, le vecteur d'ouverture de dépôt est le titre à la une. Mais la même campagne se propage aussi à travers les packages, et le cooldown est le contrôle qui aurait affamé son côté npm : une version @redhat-cloud-services fraîche, ou une dépendance transitive fraîchement empoisonnée tirée en déboguant ce dépôt Microsoft, reste en quarantaine pendant les heures exactes où elle est dangereuse.

Là où ceci ne vous sauve pas.

Un push que vous approuvez est un push qui a lieu.

Le guardrail lecture/écriture attrape l'écriture dont l'agent ne vous a pas parlé. Il ne lit pas le diff. Si vous poussez légitimement vers functions-action et que vous approuvez l'invite, Bromure forwarde le push — y compris, en principe, un workflow empoisonné que vous n'avez pas remarqué dans le diff. Lisez ce que vous approuvez. La trace de session vous dit quels diffs lire.

Le cooldown est une fenêtre, pas un mur.

Deux jours est calibré sur l'écart publication-retrait observé, mais un attaquant patient peut s'asseoir sur une version compromise plus longtemps que le cooldown et rester installable le troisième jour. Le cooldown affame les vers du jour même ; il ne se porte pas garant d'un package qui a simplement vieilli. socket.dev et OSV doivent encore faire leur part.

Le profil est de longue durée, donc la persistance persiste.

Un profil Bromure n'est pas un disque jetable. Une charge utile qui s'écrit dans un chemin de démarrage à l'intérieur du profil peut survivre jusqu'à la prochaine session dans ce profil. Ce à quoi elle se réveille, c'est un invité sans clés d'hôte et un courtier qui ne parle qu'en tokens à courte durée, sur invite, à portée limitée — une présence dans une boîte qui ne contient rien — mais une présence tout de même.

Scopez le courtier exprès.

Si un profil est provisionné pour pousser vers un dépôt aujourd'hui et que ce profil exécute Miasma aujourd'hui, une écriture approuvée passe. Les octrois du courtier fonctionnent parce qu'ils sont étroits. Un profil qui n'a besoin que de lire un dépôt ne devrait pas pouvoir y écrire ; un profil qui ne publie jamais ne devrait détenir aucun token de publication. L'isolation contient l'explosion ; le scoping décide de la taille qu'elle aurait pu atteindre.

Le prochain dépôt de confiance est déjà cloné quelque part.

La leçon du ver TanStack était que le lockfile et la signature ne sont pas des défenses. La leçon du scope Red Hat était que l'éditeur non plus. Microsoft ajoute le corollaire suivant : le dépôt non plus, et le déclencheur n'a même pas besoin d'être une installation que vous avez choisie — ce peut être un dossier que votre agent a ouvert. Le dépôt Azure/functions-action n'a rien fait de mal en étant de confiance. Être de confiance est tout le but d'une Action de première partie canonique, et c'est exactement ce qui l'a rendu digne d'être empoisonné — deux fois, dans le cas de durabletask.

Vous ne pouvez pas régler cela en faisant confiance plus soigneusement, parce que la confiance n'a jamais été mal placée. Vous le réglez en arrangeant les choses de sorte que « quel dépôt est-ce » et « quel scope a publié ceci » cessent d'être les questions dont dépend votre trousseau. Bromure Agentic Coding est la configuration où l'agent ouvre le dépôt à l'intérieur d'une VM par profil, les vrais credentials restent sur l'hôte derrière un courtier, chaque écriture que l'agent fait doit passer une invite, et un package ne peut pas être installé tant qu'il n'a pas survécu à un cooldown. Le pire qu'une ouverture de dossier empoisonnée puisse faire est d'exfiltrer une boîte qui n'a jamais détenu vos clés. C'est gratuit, open-source, et livré aujourd'hui.