Personne n’a publié de nouvelle version
L’avis de Wordfence du 9 août sur la compromission de BdThemes décrit une attaque de chaîne d’approvisionnement sans chaîne d’approvisionnement. Aucune version publiée, aucun script d’installation, aucun typosquat, aucun fichier modifié dans le dépôt de l’extension. Les attaquants ont pris un accès en écriture au bucket de stockage de l’éditeur et modifié un flux JSON que 350 000 sites récupéraient à chaque chargement de page d’administration. Pour un développeur, la question que cela pose est celle de ce que la machine qui fait tourner votre agent de code a le droit d’écrire, et auprès de qui.
Chaque contrôle de chaîne d’approvisionnement que vous possédez inspecte des choses qui ont été publiées. Celui-ci n’a rien publié, et a atteint 350 000 sites quand même.
Le 7 août 2026, Wordfence a commencé à entendre parler de sites WordPress équipés d’extensions BdThemes où poussaient des comptes administrateur que personne n’avait créés. La liste compte sept extensions : Element Pack Addons for Elementor, Prime Slider, Pixel Gallery, Ultimate Post Kit, Ultimate Store Kit, Live Copy Paste et Smart Admin Assistant. Element Pack à lui seul dépasse les 100 000 installations actives, et les sept réunies franchissent les 350 000.
Au 8 août, l’éditeur avait nettoyé son API et WordPress.org avait retiré les sept extensions le temps de l’enquête. Wordfence a publié son PSA sur la compromission le lendemain, et sa conclusion centrale est une négation : personne n’avait touché au code des extensions.
Rien n’a été publié
Les extensions BdThemes embarquent un composant interne appelé Biggopti dont le rôle est modeste et ennuyeux : récupérer des bannières promotionnelles depuis l’API distante de l’éditeur et les afficher dans le tableau de bord d’administration de WordPress. Les données de bannière sont un fichier JSON statique dans un bucket DigitalOcean Spaces, servi derrière Cloudflare.
Les attaquants ont obtenu un accès en écriture à ce bucket. C’est toute l’intrusion.
Biggopti prenait le champ display_id de la réponse JSON et le déposait dans un
attribut HTML id sans l’échapper, une faille de cross-site scripting que
Wordfence a notée
CVSS 5.4, moyenne
et fait remonter au 1er mars 2026, quand un développeur l’a écrite une fois puis
recopiée d’une extension à la suivante. Un display_id porteur d’un
gestionnaire onanimationstart est donc du JavaScript, et il s’exécute dans le
navigateur de chaque administrateur connecté, à chaque chargement de page
wp-admin, sur chaque site où l’extension est installée.
La charge utile a bien exploité ce siège. Un script nommé w2.js demandait ses
instructions de ciblage à un serveur de commande et de contrôle, la machine
qu’un attaquant utilise pour indiquer à une charge utile quels sites valent la
peine d’être pris, puis créait des comptes administrateur cachés via l’API REST
de WordPress, sur le dos de la session et des nonces du véritable
administrateur. Une variante dérivait des noms d’utilisateur prévisibles du nom
d’hôte du site. Une fausse extension, souvent nommée quelque chose comme
wp-smart-thumbnails, livrait un web shell à emer-run.php, c’est-à-dire un
script qu’un attaquant pilote en chargeant une URL ordinaire. Des must-use
plugins, que WordPress charge à chaque requête et masque de l’écran des
extensions, portaient une porte dérobée à connexion magique et un module qui
dissimulait les nouveaux comptes en réécrivant les totaux d’utilisateurs
qu’affiche l’écran d’administration.
Les horodatages des enregistrements empoisonnés placent le début le plus ancien possible au 23 juin 2026, sept semaines avant que quiconque ne remarque quoi que ce soit.
Alignez cela sur les contrôles qu’une équipe rigoureuse fait tourner. Épinglage des versions : les versions n’ont jamais changé. Lockfiles et empreintes d’intégrité : elles concordaient, parce que les fichiers qu’elles couvraient étaient intacts. Revue des scripts d’installation : il n’y a pas eu d’installation. Détection de typosquat : bon nom, bon éditeur, bonne entrée de dépôt. Revue de code du diff entre versions : pas de diff, puisque pas de version. Certificat et CDN : tous deux en parfaite santé. Cloudflare et TLS ont prouvé que le JSON arrivait au site sans modification depuis le bucket d’où il était censé venir, ce qui est précisément ce qui le rendait utile.
L’incident commence par un identifiant qui pouvait écrire
Lisez l’avis du côté de l’éditeur et la chaîne d’attaque se réduit à une étape. Quelqu’un a obtenu un accès en écriture à un bucket de stockage. Tout ce qui suit est une conséquence.
Les compromissions en amont arrivent sans cesse sous cette forme. Le ver du scope npm de Red Hat, le dépôt qui était vraiment celui de Microsoft, les places de marché d’extensions, les registres de conteneurs : dans chacun d’eux, la charge utile est la partie qui se fait raconter, et la partie qui comptait, c’est quelqu’un qui s’est retrouvé avec un identifiant capable d’écraser un artefact que d’autres gens récupèrent à intervalles réguliers.
Où vit un identifiant pareil ? Dans l’environnement d’un développeur. Un
DIGITALOCEAN_ACCESS_TOKEN dans un shell, une configuration doctl dans
~/.config, un login de registre dans ~/.docker/config.json, un profil AWS,
un jeton GitHub dans ~/.git-credentials, une clé SSH qui peut pousser. Sur un
portable, dans un terminal, à côté du projet. En 2026, à côté du projet, c’est
aussi là que vous faites tourner votre agent de code.
La plupart des articles de ce blog décrivent une façon de retourner cet agent contre vous. Un rapport de bug qui a exécuté une commande. Une page web qui a réécrit la configuration de l’agent. Un commentaire que personne ne pouvait voir. Une compétence qui s’est exécutée avec votre identité. Trois contournements de harnais dans une seule conférence Black Hat. Prenez le catalogue pour acquis, et supposez qu’un mardi l’un d’eux vous tombera dessus.
À quel point ce mardi tourne mal dépend d’une question à laquelle vous pouvez répondre aujourd’hui, sans savoir quelle injection l’emportera : qu’est-ce que cette machine a le droit d’écrire, et auprès de qui ? Sur un portable, la réponse est tout ce que vous avez le droit d’écrire, et si vous publiez quoi que ce soit, cela inclut l’artefact que plusieurs centaines de milliers d’inconnus récupèrent sans regarder.
Le jeton n’est pas dans la machine
Bromure Agentic Coding fait tourner l’agent dans une VM Linux jetable sur Apple Silicon, et chaque octet de son trafic traverse un proxy sur l’hôte, hors de la boîte où tourne l’agent. Bromure dispose les identifiants autour de cette frontière au lieu de les mettre à l’intérieur.
Mettez votre jeton d’accès personnel DigitalOcean dans le panneau
Credentials et Bromure garde la vraie valeur sur le Mac. Ce qui atterrit
dans la VM est un leurre : une variable d’environnement
DIGITALOCEAN_ACCESS_TOKEN et un ~/.config/doctl/config.yaml qui font marcher
doctl sans doctl auth init, avec un espace réservé dedans. Quand la VM émet
une requête vers api.digitalocean.com, le proxy de l’hôte substitue le vrai
jeton sur le fil, pour cette destination, et rien d’autre.
Tout ce qui tourne dans cette VM peut lire l’environnement, greper les dotfiles
et parcourir le disque entier : l’agent, une dépendance, une étape de build, une
commande shell sortie d’un ticket empoisonné. Ce qu’il récolte, c’est brm_….
Le même arrangement couvre le reste du lot : jetons GitHub, GitLab et Bitbucket,
logins de registres de conteneurs écrits sous forme de faux blob base64 dans
~/.docker/config.json, un kubeconfig synthétique avec des certificats client
jetables, Linear, les points d’accès HTTPS de bases de données, et tout ce que
vous ajoutez d’autre sous Other API keys. AWS va plus loin : l’hôte resigne
chaque requête en SigV4 avec le vrai secret, si bien que contourner le proxy
rapporte une InvalidSignatureException plutôt qu’une écriture non autorisée.
Guardrails décide du verbe, sur le Mac
Cet incident avait besoin d’une seule opération : écraser un objet dans un
bucket. Guardrails est un moteur de politiques à l’intérieur du proxy de
l’hôte, avec un mode par ressource : Off, Block destructive ou Read-only.
Réglez DigitalOcean sur Read-only et chaque mutation vers
api.digitalocean.com revient en 403 franc que l’agent rapporte comme un
échec d’API ordinaire. Bromure prend cette décision dans macOS. Il n’y a
aucun réglage à changer dans la VM, aucune variable d’environnement à
désactiver et aucun fichier à modifier.
Publier demande d’abord
Chaque entrée d’identifiant a Require approval to use. Activez-la et chaque échange leurre→vrai lève une boîte de dialogue de consentement sur l’hôte avant que la vraie valeur n’atteigne le fil, avec des autorisations limitées dans le temps sur le chemin SSH : cinq minutes, une heure, le reste de la session. Pousser une version, c’est quelque chose que vous vouliez faire, et cela vous coûte un clic. Une écriture que vous n’avez pas lancée apparaît comme une boîte de dialogue à laquelle vous ne vous attendiez pas, à un moment où vous n’en attendiez aucune.
Registres et git ont le même traitement
Guardrails classe le trafic des registres de conteneurs par méthode, contre
le nom d’hôte du registre lui-même : GET et HEAD sont un pull, PUT et
POST sont un push, DELETE est destructif. GitHub, GitLab et Bitbucket
sont couverts à la fois pour l’API REST et pour git sur HTTPS, où
git-receive-pack compte comme une écriture et est bloqué en mode Read-only
alors que les fetches passent toujours. Un profil qui lit tout votre graphe
de dépendances et ne pousse rien, c’est deux réglages.
L’environnement est une liste que vous avez écrite
Un profil Bromure voit les dossiers du Mac que vous avez partagés avec lui,
jusqu’à huit, chacun monté sous /home/ubuntu. Tout le reste sur votre Mac
n’est pas monté, pas joignable et pas énumérable. Le jeu d’identifiants est
celui que vous avez ajouté à ce profil, si bien qu’un profil dont vous vous
servez pour construire un frontend ne contient aucun identifiant de cluster
à trouver, caché ou non.
La récupération à l’exécution traverse le proxy elle aussi
Prenez maintenant le versant aval de l’histoire, celui où vous êtes l’un des 350 000. Ce que vous pouvez y attraper dépend de l’endroit où vous placez l’inspection.
Le panneau Supply Chain de Bromure filtre les récupérations de paquets sur
npm, PyPI, Cargo, RubyGems, Maven, NuGet, les modules Go et Packagist : une
barrière d’âge activée par défaut qui refuse tout ce qui a été publié dans les
deux derniers jours, des recherches OSV, socket.dev ou Delpi comme fournisseur
de filtrage, le retrait des scripts d’installation qui réécrit le tarball et
corrige l’empreinte des métadonnées du registre pour que npm vérifie toujours,
et une invite avant que les tarballs épinglés par lockfile ne passent
inchangés. Les .npmrc et pip.conf à l’intérieur de la VM peuvent resserrer
ces règles et ne peuvent pas les desserrer.
Ces règles paient leur écot à cause de l’endroit où Bromure les applique. Le proxy de l’hôte les applique à une requête plutôt qu’à un paquet, si bien qu’un fichier JSON tiré d’un bucket d’éditeur à l’exécution par une étape de build, une CLI, un serveur MCP ou l’application elle-même traverse la même frontière qu’un tarball. Tout ce que la VM dit au réseau la traverse.
Cela change ce à quoi ressemblent les sept semaines. Avec Session trace sur Activity, Bromure enregistre l’hôte, le statut, la latence, le rapport de substitution et les éventuels avertissements de fuite pour chaque requête que fait la VM. Réglez-le sur Everything et il garde aussi les corps, pour chaque hôte, chiffrés avec la même clé de trousseau que les secrets de votre profil et lisibles dans le Trace Inspector. Le jour où un flux dont dépend votre build se met à répondre différemment est un diff que vous pouvez ouvrir, sur le Mac, dans un journal où la VM n’a aucun accès en écriture.
Un flux empoisonné est aussi, au bout du compte, du texte qui arrive sur une
machine. Quand il atteint l’agent sous forme de tool_result, de fichier, de
page récupérée ou de fichier d’instructions comme CLAUDE.md ou AGENTS.md, le
panneau Prompt Injection le note sur l’appareil avec un modèle local, et
rien ne quitte le Mac. Choisissez Ask me what to do et la requête se met en
pause avec le passage signalé affiché ; choisissez Block unilaterally et
l’agent reçoit un 451 franc. Quand vous voulez que la machine disparaisse,
Erase home réinitialise /home/ubuntu et Reset to base reclone le
disque système du workspace depuis l’image de base.
BdThemes corrigera l’échappement, le bucket recevra des clés plus strictes, et la prochaine version de cette histoire arrivera avec un autre éditeur et un autre flux. Ce qui dure, c’est l’arrangement en dessous. Votre environnement de développement détient une poignée d’identifiants, dont chacun peut réécrire quelque chose que des milliers de gens récupèrent à intervalles réguliers, et ce même environnement fait maintenant tourner un agent qui lit du texte d’inconnus toute la journée.
La plupart des articles ici demandent ce que votre agent devrait avoir le droit de lire. Répondez aussi à l’autre question, celle qui est inconfortable sur un portable et facile dans un profil : qu’a-t-il le droit d’écrire, et auprès de combien de gens ?
Installez Bromure Agentic Coding, et donnez à l’agent une machine qui ne détient la clé de rien de ce que vous publiez.