Principaux enseignements
- Hades intégrait des hooks de démarrage au format .pth, de sorte que la charge utile se déclenchait à chaque démarrage de Python plutôt qu'une seule fois lors de l'installation.
- Cette astuce avait déjà été intégrée à LiteLLM onze semaines plus tôt ; cette technique n'était donc pas nouvelle.
- Les « Wheels » ne passent pas par l'étape de compilation du code source, mais elles peuvent tout de même créer un fichier qui exécute du code à chaque démarrage de l'interpréteur.
- Pour analyser votre propre environnement, il suffit de quelques commandes, et la plupart des fichiers .pth que vous trouverez seront légitimes.
Un développeur exécute la commande « pip install » ; celle-ci se termine normalement et le job d'intégration continue (CI) passe au vert. Rien n'a été compilé et aucune étape de compilation du code source n'a été exécutée, car le paquet était fourni sous forme de fichier « wheel », et les fichiers « wheel » étaient censés constituer l'option la plus sûre.
Ensuite, le processus Python suivant démarre. Un programme lit le fichier ~/.aws/credentials.
Voilà en gros les grandes lignes de la campagne « Hades », révélée par Orca Security le 8 juin 2026 sur 26 paquets PyPI. Socket, qui a examiné les mêmes 37 fichiers Wheel malveillants, en recense 19 ; le chiffre avancé dépend donc de la source que l’on consulte. Quoi qu’il en soit, ce code contourne plusieurs contrôles spécialement conçus pour limiter l’exécution de code lors de l’installation, non pas en exploitant un bug, mais en utilisant une fonctionnalité présente dans la bibliothèque standard bien avant que la plupart de ces contrôles n’existent.
Anatomie de la campagne « Hades »
Ce qui distingue ce cas des autres cas courants de cas de typosquatting n’est pas une astuce en particulier. C’est la combinaison de plusieurs éléments.
Il ne s'agissait pas de faux noms ressemblants destinés à profiter d'une erreur de saisie lors d'une installation. Le schéma de publication semble indiquer une prise de contrôle du compte d'un responsable de maintenance, suivie d'une vague de publications consécutives de correctifs sur l'ensemble du portefeuille de ce responsable, principalement dans les domaines du Python scientifique, de la bio-informatique et des outils d'apprentissage automatique. Des paquets auxquels les équipes faisaient déjà confiance ont commencé à diffuser de nouvelles versions corrompues.
Chaque version compromise plaçait directement dans le répertoire « site-packages » un fichier dont le nom correspondait à *-setup.pth. Au lieu de cibler les portefeuilles cryptographiques ou les cookies de navigateur, comme le font de nombreux logiciels d’espionnage, la charge utile visait les identifiants de connexion au cloud, les secrets de développement et token CI/CD . Le hook de démarrage n’est que la première étape. Il télécharge le runtime JavaScript Bun depuis GitHub et exécute une charge utile JavaScript obfusquée nommée _index.js. « Un hook de démarrage Python amorçant un runtime JavaScript pour voler des clés AWS » : voilà une phrase étrange à écrire, mais qui décrit parfaitement ce qui s’est passé.
Certains échantillons comportaient également un démon de persistance appelé « gh-token-monitor », qui menace de mener des actions destructrices si les jetons volés sont révoqués. Un logiciel malveillant conçu pour vous punir d'avoir mis en œuvre une réponse adéquate à un incident est suffisamment inédit pour qu'on s'y attarde un instant.
Hades n'est pas apparu de nulle part. Il s'inscrit dans la continuité du compromis LiteLLM de mars 2026 et de la compromission du paquet durabletask signalée en mai 2026, ce qui porte à trois le nombre d’attaques notables visant la chaîne d’approvisionnement de PyPI sur une période de moins de trois mois. Ces trois incidents ont été associés à la même campagne plus large, suivie sous des noms tels que TeamPCP, Mini Shai-Hulud et Miasma.
| Incident | Signalé | Mécanisme |
| LiteLLM | mars 2026 | Malveillant fichier .pth fichier malveillant diffusé après la compromission du pipeline de publication de LiteLLM |
| durabletask (Microsoft) | Mai 2026 | Un jeton de publication PyPI volé a été utilisé pour publier directement trois versions |
| Hadès | juin 2026 | .pth hooks de démarrage dans 26 paquets, vol d'identifiants, propagation de type ver |
C'est cette ligne « LiteLLM » qui pose problème. L'astuce consistant à exploiter les « hooks » des start-ups n'était pas nouvelle en juin. Elle avait déjà été intégrée en mars dans un paquet générant des millions de téléchargements par jour, sous la forme d'un fichier nommé « litellm_init.pth », et onze semaines plus tard, cette technique permettait toujours à des programmes malveillants de contourner les contrôles mis en place pour empêcher leur exécution au moment de l'installation.
Pourquoi un fichier .pth s'exécute avant votre code ?
Voici le mécanisme, et il est assez impressionnant, comme le sont parfois les choses laides.
Lors d'un démarrage normal, le module `site` de Python traite les fichiers `.pth` qu'il trouve dans les répertoires de site configurés. Officiellement, cela sert à ajouter des répertoires à `sys.path`, à raison d'un par ligne. Mais ce même module présente un deuxième comportement, bien moins connu. Toute ligne commençant par `import` suivi d'un espace ou d'une tabulation est exécutée en tant que code Python.
Il suffit donc d'une seule ligne.
import os, base64 ; exec(base64.b64decode('…'))
Placez ce fichier dans un dossier avec l'extension .pth à l'intérieur du répertoire `site-packages` : nul besoin de l'importer ou de l'appeler. Il s'exécute à chaque démarrage d'un processus Python dans cet environnement. Sur un serveur d'intégration continue très sollicité ou un ordinateur portable de développement, cela peut se produire des dizaines de fois par jour ; il se déclenche notamment lors de l'exécution de `pip`, de `python -c`, ainsi que lors de toute opération effectuée en arrière-plan par le serveur de langage de votre éditeur.
Le saviez-vous ? Il ne s'agit ni d'un bug ni d'une faille exploitée de manière ingénieuse. C'est un comportement documenté dans le module « site » de Python, qui précède de plusieurs années les outils de packaging modernes. MITRE le répertorie désormais comme une technique d'attaquant identifiée, les « Python Startup Hooks », sous-technique T1546.018 dans la catégorie « Event Triggered Execution ».
Setuptools utilise des fichiers .pth pour certaines installations modifiables, et coverage.py peut recourir au même mécanisme pour lancer automatiquement coverage au sein de sous-processus. C’est précisément pour cette raison qu’il s’avère si efficace en tant que vecteur d’attaque. Un scanner cherchant à signaler un fichier .pth malveillant doit, d’une manière ou d’une autre, le distinguer du fichier distutils-precedence.pth, fourni avec setuptools et qui remplit structurellement la même fonction pour des raisons tout à fait légitimes.
Pourquoi les « Wheels » étaient censées être le format le plus sûr
Depuis des années, on recommande généralement de privilégier les fichiers « wheel » aux distributions source, et ce raisonnement reste tout à fait valable. L'installation d'un sdist implique généralement de le compiler au préalable, et cette compilation exécute du code contrôlé par le gestionnaire de paquets via un backend conforme à la PEP 517, ce qui revient, quelle que soit la manière dont on présente les choses, à une exécution de code arbitraire. Un fichier « wheel » est quant à lui déjà compilé ; son installation se résume donc principalement à le décompresser et à placer les fichiers.
Ce conseil reste valable, et cette méthode a de toute façon fonctionné. Un fichier « wheel » peut contenir un fichier .pth, et le décompresser place automatiquement tout fichier .pth de premier niveau directement dans le répertoire « site-packages ».
L'exécution se fait simplement avec un certain délai.
| Mécanisme | Lorsque le code s'exécute | S'exécute lors de l'installation ou de la compilation |
| Backend de compilation sdist | Lors de l'installation, au moment de la compilation | Oui |
| npm postinstall | Pendant l'installation | Oui |
| Roue .pth crochet | À chaque démarrage de l'interpréteur, après l'installation | Non |
| Rust build.rs | Lors de la compilation | Oui |
Regardez la ligne « wheel ». Une vérification effectuée au moment de l'installation qui analyse l'environnement et ne détecte rien ne présente pas de dysfonctionnement. Au moment de l'installation, aucun programme malveillant ne s'est encore exécuté. La charge utile reste en attente et se déclenche lors de la prochaine invocation de Python, ce qui, dans le cadre de la création d'un conteneur, peut ne se produire qu'une fois que l'image a été déployée et qu'elle traite déjà le trafic.
Attention : le sandboxing au moment de l'installation et une politique « wheels-only » constituent tous deux des contrôles utiles, mais aucun des deux, pris isolément, n'empêche un hook .pth malveillant de s'exécuter ultérieurement, lorsque Python démarre en dehors de ce sandboxing. Si votre modèle de menace partait du principe que ces deux mesures couvraient l'exécution de code arbitraire, cette hypothèse doit être réexaminée.
L'un des points de contrôle les plus efficaces intervient avant même que l'artefact n'atteigne l'environnement, ce qui implique d'inspecter et de vérifier le contenu réel d'un paquet plutôt que de se fier aux informations fournies par le processus d'installation. Recompiler les paquets à partir du code source et analyser le résultat obtenu est un moyen d'y parvenir.
Ce que la charge utile cherchait à détecter
Principalement les identifiants cloud. Et l’espace de recherche est bien plus restreint et prévisible que ne le pensent la plupart des équipes, car une machine de développement ou un runner d’intégration continue a tendance à exposer des secrets précieux via une liste restreinte d’emplacements bien connus. Les identifiants et fichiers de configuration se trouvent sous ~/.aws. Les identifiants par défaut de l’application sont sous ~/.config/gcloud. Le fichier kubeconfig se trouve sous ~/.kube/config. Les informations d’authentification du registre sont dans ~/.docker/config.json. Toutes les variables d’environnement injectées par la plateforme d’intégration continue. Et sur les hôtes cloud, le service de métadonnées d’instance à l’adresse 169.254.169.254. Le rapport Hades place également les jetons GitHub, npm, PyPI et RubyGems, ainsi que les clés SSH, parmi les cibles les plus fréquentes, ce qui explique comment un voleur d’identifiants peut se transformer en un programme capable de se propager de lui-même.
Lorsqu’il est accessible, le service de métadonnées constitue le véritable enjeu. C’est ce qui peut transformer un simple piratage d’ordinateur portable en une compromission d’une partie bien plus importante de l’environnement cloud environnant. Sur un agent de compilation auquel est associé un rôle IAM, une charge utile permettant l’exécution de code réel, plutôt qu’une simple attaque SSRF, peut récupérer des identifiants temporaires pour toutes les actions autorisées par ce rôle : une simple requête GET sur les anciennes configurations IMDSv1, ou un échange de jetons via une requête PUT puis GET lorsque IMDSv2 est appliqué. Dans les deux cas, rien ne doit exister sur le disque.
C'est là toute la logique stratégique de cette approche. Les identifiants extraits d'un environnement de développement ont plus de valeur que ceux récupérés sur l'ordinateur portable d'un utilisateur, et les serveurs d'intégration continue (CI) font généralement l'objet d'une surveillance humaine moins directe que les postes de travail, alors qu'ils contiennent des secrets d'une valeur exceptionnelle.
La partie consacrée aux scanners à intelligence artificielle
Plusieurs rapports concernant Hades ont mis en évidence la présence d’injections indirectes de commandes au sein des paquets, du code spécialement conçu pour manipuler l'examen de sécurité assisté par IA plutôt que de tromper la personne chargée de lire la pull request.
C'est une nouvelle donne notable, qui n'a de sens sur le plan tactique que si les attaquants partent désormais du principe qu'un modèle de langage de grande échelle (LLM) se trouve entre le paquet et l'ingénieur. À la mi-2026, ce n'est pas une mauvaise hypothèse. Certaines équipes disposent déjà d'un modèle qui résume les différences entre les dépendances avant même qu'un humain n'y jette un œil.
Ce qui pose un problème délicat. Un modèle qui lit le contenu d’un paquet non fiable traite, par définition, des données d’entrée contrôlées par un attaquant. Il doit donc être géré de la même manière que n’importe quel autre chemin d’entrée non fiable. Ce qu’il ne doit en aucun cas endosser, c’est le rôle de juge indépendant chargé de déterminer si ce qu’il vient de lire est sûr. Considérer ce verdict comme faisant autorité revient à créer une dépendance circulaire déguisée en mécanisme de contrôle.
Conseil d'expert : Si un modèle examine le contenu d’une dépendance à n’importe quel stade de votre pipeline, considérez ce paquet comme une entrée non fiable et ne l’intégrez pas dans le contexte d’instructions du réviseur. Associez-le à des vérifications statiques déterministes et ne laissez pas le verdict du modèle être le seul critère de validation.
Comment réaliser dès maintenant un audit de votre environnement
Cela vaut la peine de le faire, que vous pensiez avoir été affecté ou non, et il s’agit simplement de quelques commandes, pas d’un projet à part entière. Si vous voulez une réponse rapide concernant l’exposition, les listes des paquets concernés sont publiées dans les rapports d’Orca et de Socket ; les comparer à vos fichiers de verrouillage est bien plus efficace que de se livrer à des conjectures.
Commencez par identifier les répertoires de sites qui sont effectivement traités. C'est important, car le répertoire « site-packages » propre à chaque utilisateur est également pris en compte, et une liste ne portant que sur le système ne le tiendrait pas compte sans que cela soit visible.
python -m site
Ensuite, sous Linux ou macOS, recherchez les fichiers .pth contenant des lignes exécutables. Sous Windows, il faut effectuer la même recherche dans les répertoires « site-packages » de vos installations Python et de vos environnements virtuels.
find / -path « *site-packages* » -name « *.pth » \
-exec grep -lE “^import[[:space:]]” {} \; 2>/dev/null
Cette classe d’espaces a son importance, car `site` n’exécute que les lignes où `import` est suivi d’un espace ou d’une tabulation, et un simple `^import` correspondra sans problème à une ligne tout à fait innocente commençant par `importlib`. Tant que vous y êtes, le passage spécifique à Hades consiste à rechercher tout ce qui correspond à *-setup.pth ; il convient également de jeter un œil à `sitecustomize.py` et `usercustomize.py`, puisqu’ils appartiennent à la même famille de hooks de démarrage. Certains exemples Hades auraient injecté du code dans `__init__.py` au lieu de fournir un fichier `.pth`, de sorte qu’un balayage limité aux fichiers `.pth` ne garantit pas pour autant l’absence totale de problèmes.
Vous obtiendrez des résultats, et c’est normal. Attendez-vous à trouver des fichiers « distutils-precedence.pth » provenant de setuptools, des fichiers « __editable__*.pth » issus d’installations modifiables, et peut-être un ou deux provenant de bibliothèques scientifiques. Lisez-les quand même. Les fichiers .pth légitimes contiennent généralement des entrées de chemin d’accès ou de petits hooks d’importation générés par des outils que vous pouvez identifier. Les blobs encodés, les appels à exec(), les lancements de sous-processus ou tout ce qui tente d’accéder au réseau méritent d’être examinés de beaucoup plus près, même si aucun modèle particulier ne permet à lui seul de prouver qu’un fichier est malveillant.
Pour déterminer si le traitement côté serveur est à l'origine d'un comportement que vous essayez d'analyser, lancez l'interpréteur en le désactivant.
python -S -c « print('traitement du site ignoré') »
Si le problème disparaît avec l'option -S, cela indique que l'initialisation du site est en cause et qu'il convient notamment d'examiner les hooks .pth. Cela permet de restreindre le champ de recherche. Cela ne prouve toutefois pas quel mécanisme de démarrage était en cause, car l'option -S désactive bien plus que l'exécution des fichiers .pth.
La vérification d'une roue avant qu'elle n'entre en contact avec l'environnement nécessite deux commandes.
pip download –no-deps somepackage
unzip -l somepackage-*.whl | grep « \.pth »
Un fichier « wheel » contenant un fichier .pth n’est pas automatiquement malveillant, et un fichier .pth n’exécute pas automatiquement quoi que ce soit, puisque seules les lignes commençant par « import » suivi d’un espace sont exécutées. Cependant, un fichier « wheel » en contenant un mérite qu’on examine son contenu, et s’il contient une ligne « import » exécutable, il vaut mieux en discuter avant qu’il ne soit ajouté à votre fichier de verrouillage, plutôt qu’après.
Si vous détectez un problème, supprimez le paquet, puis videz le cache de `wheel`, car un fichier `wheel` corrompu présent dans le cache de `pip` ou d’`uv` se réinstallera sans autre la prochaine fois qu’une requête portera sur cette version. Ensuite, effectuez une rotation de tous les éléments accessibles par l’environnement. Clés cloud, jetons GitHub, PyPI et npm, clés SSH, kubeconfigs. Les recommandations d’Orca concernant Hades considèrent la suppression des paquets et la rotation des identifiants comme obligatoires plutôt que facultatives, et c’est la bonne approche.
Pourquoi « pip install » n'est pas le bon point de départ pour accorder sa confiance
LiteLLM, durabletask et Hades présentent des différences dans les détails, mais le principe sous-jacent est identique. Aucun d’entre eux n’a eu besoin d’exploiter une vulnérabilité dans l’application utilisant le paquet. Chacun a détourné une partie du pipeline de packaging que le système considère comme fiable par conception, qu’il s’agisse d’un processus de publication compromis chez LiteLLM, d’un jeton de publication volé chez durabletask, ou d’identités de publication détournées associées à un hook de démarrage documenté chez Hades.
Ce modèle exclut notamment le « hash pinning ». Le fichier malveillant litellm_init.pth avait été correctement déclaré dans le RECORD propre au « wheel », avec un hachage correspondant ; ainsi, la commande « pip install –require-hashes » aurait fonctionné sans problème. Les hachages prouvent que l’artefact que vous avez téléchargé correspond à celui que vous avez approuvé. Ils ne prouvent pas que l’artefact approuvé était inoffensif, ni que son éditeur n’avait pas été compromis.
Concrètement, la réponse varie en fonction de la version de Python que vous utilisez.
Quelques conseils ? Vérifiez vos fichiers .pth et prévoyez une brève période de mise en quarantaine pour les nouvelles versions avant de les adopter, car ces campagnes ont tiré une grande partie de leur portée initiale de versions malveillantes récemment publiées, avant que les défenseurs n’aient eu le temps de réagir.
Vous utilisez Python sur un parc informatique de grande envergure ? L'audit constitue un bon point de départ, mais la meilleure approche architecturale consiste à déplacer la vérification des paquets en amont de l'installation, afin de réduire d'emblée le risque qu'un artefact malveillant parvienne jusqu'au répertoire « site-packages ».
Et si vos runners d'intégration continue (CI) détiennent des identifiants de rôles cloud, commencez par là. Limitez strictement la portée de ces rôles, supprimez les identifiants statiques à longue durée de vie et utilisez des identifiants à courte durée de vie partout où la plateforme vous le permet, car c'est là que se situe la zone d'impact lorsqu'une installation défectueuse se transforme en incident.
Sécurisation de la chaîne d'approvisionnement Python
Le fait de déplacer ce filtrage en amont est le principe même sur lequel repose SecureChain pour Python. Les paquets sont recompilés à partir du code source et vérifiés avant même d’atteindre votre environnement, plutôt que d’être analysés après leur exécution, à chaque démarrage de l’interpréteur. Et cette couverture ne s’arrête pas non plus le jour de l’installation.
De nouvelles vulnérabilités CVE sont divulgués concernant des paquets que vous utilisez déjà, et il arrive parfois qu’un responsable de maintenance cesse complètement de s’en occuper. SecureChain continue d’appliquer les correctifs aux éléments qui se trouvent réellement dans votre pile, et pour les paquets du catalogue, cette couverture se poursuit même après fin de vie , au lieu de vous laisser face à un choix cornélien entre une dépendance non corrigée et une réécriture forcée. Si une politique de « wheels-only » ou une analyse post-installation fait actuellement office de ligne de défense, Hades constitue une bonne occasion de vérifier où se situe réellement cette ligne, au moment de l’installation et chaque jour par la suite.
FAQ
Qu'est-ce qu'un logiciel malveillant PyPI ?
Code malveillant publié sur le Python Package Index, généralement dissimulé dans un paquet qui se fait passer pour un paquet légitime, ou dans un paquet légitime dont le compte de l'éditeur a été piraté. Il s'exécute souvent lors de l'installation ou au démarrage de l'interpréteur, plutôt que d'attendre que le code de votre application l'appelle.
Qu'est-ce qu'un fichier .pth et pourquoi est-il dangereux ?
Un fichier .pth situé dans l’un des répertoires « site » de Python est traité par le module « site » lors du démarrage. Les lignes commençant par « import », suivies d’un espace ou d’une tabulation, sont exécutées, ce qui signifie qu’un fichier .pth peut exécuter du code arbitraire à chaque démarrage d’un processus Python. Il s’agit d’un comportement documenté et prévu, et tant setuptools que les installations modifiables l’utilisent de manière légitime, ce qui rend justement les fichiers malveillants difficiles à détecter.
Les « wheels » offrent-elles une protection contre les paquets Python malveillants ?
Pas tout à fait. Les « wheels » permettent d'éviter l'étape de compilation à la source requise par l'installation d'un sdist, et cette étape peut exécuter du code contrôlé par le paquet. Elles n'empêchent toutefois pas un paquet de fournir un fichier .pth contenant une ligne d'importation exécutable qui s'exécute à chaque démarrage de l'interpréteur.
Comment puis-je vérifier si un paquet a installé un hook de démarrage ?
Recherchez dans les répertoires « site » utilisés par Python pour les fichiers .pth les lignes commençant par « import » suivies d'un espace, puis lisez-les correctement. L'exécution de l'interpréteur avec l'option -S désactive l'initialisation automatique de « site », y compris le traitement des fichiers .pth ; cela permet donc de déterminer si le comportement au démarrage est en cause, sans pour autant prouver quel mécanisme en est à l'origine.
Est-ce que pip-audit ou un scanner détecte ce problème ?
Cela dépend de l'outil. Les scanners basés sur des recommandations s'appuient sur leurs bases de données de vulnérabilités et de paquets malveillants sous-jacentes ; par conséquent, une version malveillante récemment publiée peut ne pas être détectée immédiatement. Les outils basés sur le comportement et les artefacts permettent parfois de détecter les menaces plus rapidement, et la couverture varie considérablement d'un éditeur à l'autre.




