Le flag a dit non
Le 3 août 2026, The Hacker News couvrait FaceHugger : trois failles dans la bibliothèque Diffusers de Hugging Face — sept millions de téléchargements par mois — qui permettent à un dépôt de modèle piégé d'exécuter du code arbitraire sur la machine qui le charge, même avec trust_remote_code=False. La cause racine : la vérification de confiance s'exécutait à un autre moment que le chargement du code. Les agents de code récupèrent des modèles comme ils récupèrent des paquets, donc le flag qui mentait gardait des laptops de développeurs. Bromure Agentic Coding place la frontière sous le processus plutôt qu'à l'intérieur : le chargement tourne dans une VM jetable, les identifiants à portée sont des leurres, et l'appel maison du payload meurt au proxy hôte — le dépôt malveillant coûte à l'attaquant son payload et ne vous coûte rien.
Le dépôt contenait un fichier nommé None.py, et c'était toute l'astuce.
Le flag de sécurité était activé, la vérification a tourné, et le code
s'est exécuté quand même, parce que la vérification et le chargement se
produisaient à deux moments différents — et l'attaquant n'avait qu'à être
présent au second.
Le 3 août 2026, The Hacker News
a relayé la recherche que Zafran Labs a publiée fin juillet :
trois vulnérabilités dans la bibliothèque Diffusers de Hugging Face,
collectivement baptisées FaceHugger, qui permettent à un dépôt de
modèle piégé d'exécuter du code Python arbitraire sur toute machine qui le
charge. Diffusers est la voie standard pour exécuter des modèles de
diffusion — environ sept millions de téléchargements par mois, installés
dans des pipelines de production, des jobs CI, des notebooks et des images
de conteneurs. La bibliothèque a un garde-fou pour exactement ce
scénario : trust_remote_code, un flag censé empêcher du code non relu
venant d'un dépôt de modèle de s'exécuter sur votre machine. Les trois
failles passent devant lui alors qu'il est réglé sur False.
La vérification et le chargement étaient deux événements distincts
Charger un modèle depuis le Hub n'est pas une opération unique. La
bibliothèque effectue deux requêtes HTTP séquentielles : la première
récupère la configuration du dépôt, la seconde récupère le code et les
poids que cette configuration désigne. Le portail trust_remote_code
faisait son inspection pendant la première requête. Ce contre quoi il
montait la garde arrivait dans la seconde.
Les chercheurs de Zafran ont trouvé trois chemins à travers cette brèche,
désormais dotés de CVE
et corrigés dans Diffusers 0.38.0. Dans CVE-2026-44827 (CVSS 8.8), une
bizarrerie de formatage de chaîne construit le nom de fichier None.py
quand aucun pipeline custom n'est demandé — et le test d'existence de ce
fichier tourne sur un chemin de code qui ne le signale pas, si bien qu'un
dépôt qui embarque un fichier nommé None.py franchit le portail et
s'exécute au chargement. CVE-2026-45804 est une course : environ un tiers
de seconde sépare les deux requêtes, et un dépôt qui change entre les deux
est validé comme une chose et exécuté comme une autre. CVE-2026-44513
saute le portail sur les chargements de snapshots locaux. Les chercheurs
de Zafran ont compressé les trois en une phrase : toute méthode qui fait
voir au chargeur du code custom que le portail n'a pas vu permet le
contournement.
Le motif a un passé. Transformers — l'autre pilier de l'écosystème Hugging
Face — a eu son propre contournement de trust_remote_code=False
(CVE-2026-4372), exploitable via un appel standard de chargement de
modèle. La classe de bug vient avec le design : une promesse de sécurité
qui vit à l'intérieur du processus qu'elle surveille. Le flag est un
argument de fonction, tandis que l'application est éparpillée entre
chemins de code, formatage de noms de fichiers et timing de requêtes.
Quand l'un d'eux contredit le flag, le flag perd, et rien ne vous dit
qu'il a perdu. Le modèle se charge et la démo fonctionne.
Votre agent charge des modèles un mardi
Relisez cette histoire depuis la chaise d'un agent de code. « Récupère le
pipeline d'inpainting sur le Hub et benchmarke-le contre le nôtre » est
une tâche de mardi ordinaire. Le projet épingle diffusers==0.37, parce
que les projets épinglent les choses. L'agent écrit ce qu'un ingénieur
soigneux écrirait — from_pretrained(..., trust_remote_code=False) — et
la version soigneuse est la version vulnérable. On peut lire le source
d'un paquet avant de l'installer ; un dépôt de modèle, c'est de la config,
des poids et, grâce au chargeur, ce que la seconde requête HTTP décide
d'être.
Les agents ont rendu ce chemin de chargement brûlant. Ils récupèrent des modèles comme ils récupèrent des paquets, en pleine tâche et par leur nom, et les attaquants ont déjà compris que la recommandation elle-même peut être empoisonnée. Un dépôt de modèle qui détone au chargement, c'est le même coup un registre plus loin, avec un meilleur déguisement : le payload loge dans un artefact que vous classez comme données, pas comme code.
Le conseil de Zafran est de traiter les dépôts de modèles comme du code non fiable. Correct — avec un prérequis sur lequel le rapport ne s'attarde pas : un endroit où du code non fiable peut s'exécuter sans rien vous coûter. Une politique de suspicion est une discipline qu'on maintient jusqu'à ce qu'une deadline devienne bruyante. Une pièce construite pour la détonation n'a pas besoin de votre discipline.
Le chargement se produit dans la boîte
Bromure Agentic Coding est cette pièce, et elle commence là où le flag de FaceHugger a échoué : la frontière se trouve sous le processus, dans un environnement avec lequel les entrailles du chargeur ne peuvent pas discuter, appliquée que le processus coopère ou non.
Faites passer l'attaque à travers un profil. Votre agent prend la tâche de
benchmark, pins compris, et appelle le chargeur sur le dépôt malveillant.
None.py s'exécute — dans une VM Linux jetable, à un hyperviseur de
macOS. La machine que le payload possède désormais est une machine qui
existe pour cette tâche, contient l'espace de travail de cette tâche, et
sera jetée quand le travail sera mergé.
Le premier vrai geste du payload est sortant. Il le faut : récupérer un
second étage, ou expédier ce qu'il a trouvé à son opérateur. Chacune de
ces requêtes traverse le proxy hôte, où la destination est vérifiée contre
la liste que vous avez approuvée pour ce profil. huggingface.co est sur
cette liste — c'est ainsi que le modèle est arrivé. La boîte de dépôt de
l'attaquant n'y est pas. Le câble refuse, comme le portail aurait dû le
faire — sauf que ce refus ne
dépend pas de chemins de code de la bibliothèque d'accord entre eux. Le
proxy ne sait pas et ne se soucie pas de savoir quel fichier Python émet
la requête. Il vérifie la destination, ne trouve rien d'approuvé, et coupe
la connexion.
Supposons que le payload délaisse le réseau et parte faire les poches, dans les variables d'environnement et les fichiers de clés. Il trouve les leurres du broker. Les vraies valeurs vivent sur l'hôte et sont injectées au niveau du câble, uniquement pour les destinations approuvées — les mots de passe n'ont jamais été dans la VM pour être volés. Et s'il tente de rendre son séjour permanent — réécrire une config, éteindre quelque chose — les formes destructrices marquent une pause pour un dialogue de garde-fou sur l'hôte, et la persistance dans une machine dont l'espérance de vie est d'une tâche est de toute façon un mauvais échange.
Pendant ce temps, la tâche réussit. Le benchmark tourne, les chiffres reviennent, vous lisez le diff. L'attaquant a dépensé une position de chaîne d'approvisionnement pour un payload qui s'est exécuté dans une pièce sans rien à voler et sans moyen d'appeler maison. Vous n'avez rien dépensé — pas même de l'attention, à moins d'aimer lire des logs de proxy pour le sport.
Patchez la bibliothèque, gardez la pièce
La note de correction du rapport de Zafran dit aux organisations de mettre à niveau puis de partir en chasse : applications, notebooks, services d'inférence et images de conteneurs qui épinglent d'anciennes versions de Diffusers. Cette chasse n'a pas de ligne d'arrivée — quelque part dans votre arbre de dépendances, un projet épingle la version avec la brèche, et le prochain FaceHugger sera le flag d'une autre bibliothèque avec un autre contournement. Le garde et le shell n'étaient pas d'accord était cet échec dans une liste d'autorisation de commandes ; le sandbox était une phrase était cet échec dans un prompt. À chaque fois, une vérification vivait à l'intérieur de la chose qu'elle vérifiait, et la chose a gagné.
La frontière d'un profil ne partage pas ce destin, parce qu'elle ne
participe pas au processus qu'elle contient. La VM ne parse pas les
configs de pipeline. Le proxy n'a pas de chemin de code None.py. Leur
unique travail — cette machine est jetable, ce câble refuse les
destinations non listées — est le même le jour où un contournement est
publié que le jour où il est corrigé. Un CISO de Sumo Logic, commentant
cette recherche sans penser à Bromure, a nommé les défenses qui
tiennent : « les moins glamour : contrôle de sortie, segmentation et
hygiène des identifiants. »
Nous avons intégré les moins glamour dans un profil que vous cliquez.
Continuez à récupérer des modèles — c'est là qu'est le levier, et votre agent devrait les essayer pour vous. Arrêtez de laisser un argument de fonction être la seule chose entre le Hub et votre laptop. Installez Bromure Agentic Coding, donnez à l'agent un profil dont les règles tiennent au niveau du câble, et la prochaine fois que la promesse d'un chargeur casse — et une cassera — l'article de presse reste celui de quelqu'un d'autre.