Compromission de lightning sur PyPI : un voleur d’identifiants basé sur Bun dans Python
30 avril 2026
0 minutes de lectureLe 30 avril 2026, deux versions malveillantes du populaire package PyPI lightning ont été publiées. Elles affectent le framework de deep learning anciennement distribué sous le nom de pytorch-lightning. Les versions 2.6.2 et 2.6.3 contiennent un répertoire caché _runtime qui télécharge le runtime JavaScript Bun depuis GitHub à l’importation, puis l’utilise pour exécuter un voleur d’identifiants obfusqué d’environ 11 Mo. La dernière version saine est la 2.6.1, publiée le 30 janvier 2026. Cette méthode, qui consiste à remplacer ou à compléter la version publiée par un responsable de la maintenance par du code fourni par un attaquant, est appelée par Snyk Learn compromission d’un package légitime.
Pour mesurer l’ampleur du phénomène : selon pypistats.org, la distribution lightning totalise 311 027 téléchargements par jour, 2 051 273 par semaine et 7 913 890 par mois. L’ancien package pytorch-lightning, toujours installable indépendamment, ajoute 436 296 téléchargements quotidiens. PyPI a depuis mis le projet en quarantaine ; https://pypi.org/pypi/lightning/json renvoie une erreur HTTP 404 et la page du projet comporte désormais la balise meta <meta name="pypi:project-status" content="quarantined">. La capture de la Wayback Machine du 18 février 2026 conserve l’état antérieur à la compromission, où la 2.6.1 était la dernière version disponible. Le package historique pytorch-lightning n’est pas affecté et pointe toujours vers sa version saine 2.6.1.
Snyk a publié l’avis SNYK-PYTHON-LIGHTNING-16323121, qui couvre les deux versions compromises. Daté du 30 avril 2026 (publication), 29 avril 2026 (divulgation), crédit : Peter van der Zee, il attribue un score de base CVSS 4.0 de 9.3 (critique) et une classification CWE-506 (code malveillant intégré). Aucun CVE n’a été attribué. Les versions affectées sont détectées par snyk test et répertoriées dans la base de données de sécurité Snyk.
C’est le deuxième jour consécutif qu’un voleur basé sur Bun et doté d’une charge utile obfusquée d’environ 11 Mo est publié dans un écosystème de niveau 1. Hier, la campagne Mini Shai-Hulud sur npm a compromis quatre packages de l’écosystème SAP en suivant la même méthode : un chargeur Bun associé à une vaste charge utile obfusquée. La charge utile de lightning est une variante de cette approche encapsulée dans Python : au lieu de réécrire le voleur JavaScript en Python natif, les attaquants ont livré un petit téléchargeur Python qui récupère Bun et exécute le même type de blob JavaScript que celui utilisé lors de la vague npm.
Contenu du package malveillant
La wheel compromise conserve les fichiers légitimes de la bibliothèque lightning, de sorte que le framework continue de s’importer et de fonctionner. Les ajouts malveillants se trouvent dans un répertoire caché _runtime à l’intérieur de la wheel, qui contient deux fichiers importants :
start.py(SHA-2568046a11187c135da6959862ff3846e99ad15462d2ec8a2f77a30ad53ebd5dcf2) : un petit téléchargeur Python. Il récupère Bunv1.3.13depuishttps://github.com/oven-sh/bun/releases/download/bun-v1.3.13/<platform>.zip, puis exécute la charge utile du voleur dans ce runtime. La version de Bun correspond à celle du chargeur observé lors de la vague npm d’hier, ce qui constitue l’un des liens techniques les plus directs entre les deux incidents.router_runtime.js(SHA-2565f5852b5f604369945118937b058e49064612ac69826e0adadca39a357dfb5b1) : un fichier JavaScript obfusqué d’environ 11 Mo, sur une seule ligne. L’obfuscation repose sur la rotation de tableaux de chaînes, à la manière dejavascript-obfuscator, ainsi que sur un chiffrement secondaire nommé__decodeScrambled()(PBKDF2/SHA-256, 200 000 itérations, selctf-scramble-v2). Le nom de la fonction, l’algorithme, le sel et le nombre d’itérations sont identiques à ceux du chiffrement extrait des compromissions de Checkmarx et de l’interface en ligne de commande Bitwarden survenues plus tôt cette année.
La wheel 2.6.3 elle-même, lightning-2.6.3-py3-none-any.whl, a pour empreinte SHA-256 56070a9d8de0c0ffb1ec5c309953cf4679432df5a78df9aeb020fbb73d2be9fb. Cette empreinte provient d’un fichier poetry.lock trouvé en ligne, qui l’avait enregistrée avant la mise en quarantaine du package par PyPI. La wheel 2.6.2 a été mise en quarantaine trop rapidement pour être largement épinglée ; aucun fichier de verrouillage public contenant son empreinte n’a été retrouvé.
L’exécution est déclenchée à l’importation du module. Le fichier __init__.py malveillant ajoute :
Lorsqu’un processus Python exécute import lightning, le thread daemon lance la charge utile exécutée par Bun en arrière-plan, sans afficher stdout ni stderr. Il n’y a ni commande distincte, ni équivalent de postinstall, ni effet secondaire visible. Tout environnement ayant importé lightning==2.6.2 ou lightning==2.6.3 doit être considéré comme exposé, y compris un simple python -c "import lightning" dans un notebook ou une étape CI qui charge le framework pour lire sa version.
La charge utile s’exécute en trois étapes observables :
Collecte d’identifiants à l’aide d’expressions régulières ciblant les jetons OAuth/PAT GitHub (
/gh[op]_[A-Za-z0-9]{36,}/g), les jetons npm (/npm_[A-Za-z0-9]{36,}/g) et les JWT d’applications GitHub (/ghs_\d+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g). Le code interroge également les services de métadonnées cloud àhttp://169.254.169.254(AWS IMDS),http://169.254.170.2(AWS ECS) ethttps://oauth2.googleapis.com/tokeninfo, puis valide les jetons collectés auprès dehttps://api.github.com/useret dehttps://registry.npmjs.org/-/whoami.Empoisonnement de dépôts via la mutation GraphQL GitHub
createCommitOnBranch. Les commits sont signés au nom declaude <claude@users.noreply.github.com>et comportent une mentionCo-authored-by:pour faire passer les modifications malveillantes pour des actions d’Anthropic Claude Code. Les fichiers déposés dans les dépôts victimes comprennent.claude/router_runtime.js,.claude/settings.json,.claude/setup.mjs,.vscode/tasks.json,.vscode/setup.mjset.github/workflows/format-check.yml.Ver informatique dans les tarballs npm : chemins de code qui modifient les tarballs de packages en local sur la machine d’un développeur, y injectent
setup.mjs, incrémentent la version corrective et publient directement avecPUTversregistry.npmjs.org, sans passer par l’interface en ligne de commande npm. Il s’agit de la même logique d’auto-propagation que Snyk a décrite hier dans la vague Mini Shai-Hulud sur npm.
La wheel malveillante ne semble pas modifier l’API publique de lightning, ce qui correspond à l’intérêt de l’attaquant : maintenir le package installable et importable aussi longtemps que possible avant son retrait.
Comment les wheels malveillantes ont été publiées sur PyPI
Le workflow release-pkg.yml du projet lightning publie sur PyPI à l’aide d’un jeton API stocké à longue durée de vie (secrets.PYPI_TOKEN_LIGHTNING), via pypa/gh-action-pypi-publish configuré avec user: __token__. Aucun éditeur de confiance PyPI (OIDC) n’est configuré pour ce projet. Une branche distincte, fix_package_publishing, créée le 19 mars et jamais fusionnée, a explicitement supprimé permissions: id-token: write de l’étape de publication.
Cet élément est important, car les wheels malveillantes ont presque certainement été publiées à l’aide du jeton stocké lui-même, et non du workflow GitHub Actions. Deux éléments viennent étayer cette hypothèse :
Le tag Git
2.6.2existe surLightning-AI/pytorch-lightninget a été créé le 19 mars parjustusschock, mais l’exécution correspondante du workflowrelease-pkga échoué à l’étapepublish-packages. Le ticket #21681 (déposé le 20 avril, « La version 2.6.2 est absente de PyPI ») confirme que la 2.6.2 n’a pas été disponible sur PyPI pendant les six semaines qui ont suivi.Le tag Git
2.6.3n’existe pas du tout. Il n’y a nirefs/tags/2.6.3, ni publication GitHub, ni exécution de workflow associée à cette version. Pourtant, une wheellightning==2.6.3a été publiée aujourd’hui sur PyPI.
L’explication la plus simple est que l’attaquant détenait PYPI_TOKEN_LIGHTNING (à longue durée de vie, sans restriction d’audience ni approbation requise pour chaque publication) et a téléversé directement les deux wheels sur PyPI avec twine ou un outil équivalent, sans passer par le workflow GitHub Actions. L’absence de la version 2.6.2 sur PyPI pendant six semaines a servi de couverture : un développeur qui attendait cette version, annoncée depuis longtemps, n’avait aucune raison évidente de s’en méfier. Il s’agit du même problème structurel que celui décrit hier dans la demande de fusion de SAP après l’incident concernant cap-js/cds-dbs : le workflow de publication disposait de droits de publication sans étape d’approbation manuelle.
Déroulement de la divulgation sur GitHub
La chronologie de la divulgation est particulièrement visible, car le flux events/public du compte de service côté responsable de la maintenance, qui supprimait les tickets entrants, était accessible.
Le compte GitHub pl-ghost (créé le 2020-12-01T15:50:40Z, champ de l’entreprise : « PyTorchLightning & Grid.ai ») est un compte de service CI utilisé depuis longtemps, et non un compte de développeur. Ses 40 commits précédents sur Lightning-AI/pytorch-lightning suivent tous des modèles génériques ("Adding test for legacy checkpoint created with X.Y.Z" ou "docs: update ref to latest tutorials"). Son jeton d’accès personnel PAT_GHOST est référencé dans release-pkg.yml pour envoyer des mises à jour entre dépôts vers gridai/base-images après chaque publication. Par conception, le jeton du compte dispose d’un accès en écriture à plusieurs dépôts pour ces étapes automatisées.
Aujourd’hui, entre 12:40Z et 14:12Z, quatre membres de la communauté ont déposé des tickets de divulgation sur Lightning-AI/pytorch-lightning et pl-ghost les a fermés chacun en quelques minutes. Voici la séquence complète, extraite du flux /users/pl-ghost/events/public :
Horodatage ISO | Action | Détail |
|---|---|---|
| Créer / supprimer une branche |
|
| Créer / supprimer une branche |
|
| Ticket déposé | #21689 par |
| Ticket fermé | #21689 fermé par |
| Ticket déposé | #21690 par |
| Créer / supprimer une branche |
|
| Créer / supprimer une branche |
|
| Ticket fermé | #21690 fermé par |
| Ticket déposé | #21691 par |
| Ticket fermé | #21691 fermé par |
| Ticket déposé | #21692 par |
| Ticket fermé | #21692 fermé par |
| Ticket fermé | #21691 fermé une deuxième fois |
| Commentaire | Responsable de la maintenance |
| Ticket fermé | #21691 fermé une troisième fois |
Quelques éléments ressortent de cette séquence :
Les quatre branches aléatoires (
pgzicpysge,hwofzwmrto,uwpkpcgubaetdependabot/fix-deds) suivent un modèle de 10 caractères minuscules déjà associé aux tests d’accès en écriture de type ver informatique de Shai-Hulud. Les cinq branches ont été supprimées en quelques secondes et n’ont déclenché aucune exécution de workflow, ce qui laisse penser que la protection des branches par défaut a empêché les pushs directs.La branche
dependabot/fix-dedsutilise une barre oblique comme séparateur et un segment « deds » qui ne correspond pas à la configuration Dependabot réelle de Lightning-AI (qui utilise un préfixe avec tiret pour les véritables pushs Dependabot). Le nom de cette branche ne correspond donc pas à un push Dependabot légitime pour ce dépôt.L’issue #21692 a été créée par
pvdzaprès avoir vu trois issues être fermées à la suite. Son contenu intégral est le suivant : « @Borda @williamFalcon @awaelchli voir https://github.com/Lightning-AI/pytorch-lightning/issues/21691. Les issues sont automatiquement fermées par l’attaquant. »pvdzest Peter van der Zee, ingénieur chez Socket et crédité dans l’avis Snyk ; il a créé directement trois des quatre issues de divulgation.L’issue #21691 a été fermée trois fois, puis rouverte à trois reprises par le responsable
ethanwharris. Plusieurs commentaires du fil ont été supprimés parLightning-AIen tant qu’administrateur de l’organisation, ce qui concorde avec une modération par les responsables des contenus publiés par le compte suspecté d’avoir été compromis. L’issue est toujours ouverte au moment de la rédaction.
La combinaison de fermetures de fils de divulgation, de schémas de création de branches précédemment associés au ver npm et du nom de branche imitant Dependabot concorde avec l’utilisation d’un même ensemble d’identifiants compromis pour publier sur PyPI et intervenir sur le dépôt GitHub. Le point d’entrée via les identifiants d’un responsable est un schéma récurrent dans les attaques de la chaîne d’approvisionnement open source déjà couvertes par Snyk, notamment la vague de compromission de comptes de responsables npm et la compromission PyPI d’elementary-data, qui ciblait les identifiants de l’ingénierie des données.
Un autre élément de contexte : le fil d’issues GitHub contenait également un lien onion vers un site aux couleurs de Team PCP, qui publie un message signé par PGP revendiquant des liens avec LAPSUS$ et des activités d’extorsion antérieures. La signature PGP n’a pas été vérifiée de manière indépendante, et les affirmations sous-jacentes ne sont pas confirmées. Les mêmes couleurs de Team PCP sont apparues dans une autre compromission PyPI suivie par Snyk plus tôt cette année : la porte dérobée litellm du 24 mars 2026. Il existe également des éléments contraires notables dans cette filiation : lors de la compromission PyPI de xinference le 22 avril, le compte X de Team PCP a publiquement nié toute implication et accusé un imitateur d’utiliser cette marque. Les différents fournisseurs ne s’accordent pas aujourd’hui sur l’attribution de l’attaque visant lightning : Wiz estime avec un haut niveau de confiance qu’il s’agit du même opérateur que celui de la campagne plus large (en citant une clé publique RSA commune utilisée pour chiffrer les secrets exfiltrés), Aikido présente cette attaque comme la suite de « Mini Shai-Hulud », et Socket estime qu’elle est l’œuvre d’un acteur distinct employant une méthode similaire. Le lien est-il réel, opportuniste ou relève-t-il d’une fausse piste délibérée ? La question reste ouverte.
Pourquoi « Bun dans Python » est important
Le détail forensique le plus révélateur concerne le choix du runtime. La charge utile qui s’exécute après import lightning est du JavaScript, exécuté avec Bun sur la machine d’un développeur Python. Un voleur d’identifiants ciblant Python n’a pas besoin d’un runtime JavaScript, et l’intégration de Bun augmente considérablement la taille du wheel. L’explication la plus simple est la réutilisation de la charge utile : le même voleur de type router_runtime.js que celui exécuté hier par la variante de Mini Shai-Hulud dans les hooks preinstall de npm est désormais livré dans des projets Python au moyen d’un petit wrapper Python qui installe Bun et lance le code JavaScript. La signature de chiffrement de router_runtime.js (nom de fonction __decodeScrambled, PBKDF2/SHA-256 avec 200 000 itérations et sel ctf-scramble-v2) est identique à celle de execution.js de SAP, de Bitwarden CLI et des charges utiles de Checkmarx KICS, ce qui indique l’utilisation d’outils communs plutôt qu’une réimplémentation indépendante.
Cela concorde avec la filiation plus large de Shai-Hulud. Le ver Shai-Hulud d’origine, en septembre 2025, la vague suivante SHA1-Hulud en novembre 2025, qui a touché plus de 600 packages, la variante Holiday Whisper / Shai-Hulud 3.0 à la fin de l’année, ainsi que l’analyse rétrospective de Snyk sur les enseignements en matière de résilience pointaient tous vers la même capacité sous-jacente : un voleur JavaScript de type ver qui collecte des jetons, les vérifie auprès de registry.npmjs.org et utilise toute autorisation d’écriture npm trouvée pour se propager.

Attaque NPM Shai-Hulud : remédiation avec Snyk. Brève présentation de l’identification et de la correction des dépendances touchées par Shai-Hulud à l’aide de snyk test et de la base de données de sécurité Snyk.
La compromission de lightning étend cette capacité à PyPI sans réécrire le voleur. En encapsulant une charge utile JavaScript dans un chargeur Python, les outils de l’écosystème JS restent intacts tandis que la portée s’étend à un autre registre. Le déroulement des dernières 48 heures va dans ce sens : npm hier, PyPI aujourd’hui, avec une charge utile de même forme et un délai de détection comparable.
Mesures recommandées
Considérez tout système sur lequel lightning==2.6.2 ou lightning==2.6.3 a été installé et importé comme compromis. La chaîne d’exécution se déclenche à l’importation, et pas uniquement à l’installation : l’importation suffit donc à lancer la charge utile.
Épinglez ou supprimez les versions malveillantes. Bloquez
lightning==2.6.2etlightning==2.6.3dans vos miroirs de registre et revenez àlightning==2.6.1(version saine publiée le 30 janvier 2026). Ne passez pas à une version supérieure à2.6.1avant que les responsables publient une version dont l’intégrité est confirmée.Renouvelez les identifiants accessibles depuis l’environnement touché. Jetons d’accès personnels GitHub, jetons GitHub à portée affinée, jetons npm, clés de fournisseurs cloud (AWS, GCP, Azure) et tout secret présent dans les variables d’environnement de l’hôte concerné. Effectuez le renouvellement depuis une autre machine de confiance.
Recherchez des commits non autorisés sur GitHub. La charge utile effectue des commits dans les dépôts des victimes en utilisant l’identité usurpée
claude <claude@users.noreply.github.com>. Vérifiez l’activité des dépôts pour repérer les commits associés à cet auteur, les créations de branches ou les modifications de fichiers de workflow effectuées avec un compte dont les jetons ont été exposés.Examinez les journaux CI/CD et les machines des développeurs. Recherchez les connexions sortantes vers
https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/, les processusbuninattendus, les appels àhttp://169.254.169.254ouhttp://169.254.170.2depuis des machines qui ne devraient pas interroger les métadonnées cloud, ainsi que les nouveaux threads daemon lancés par des interpréteurs Python ayant importélightningpendant la période concernée.Examinez les archives npm sur les machines des développeurs. La charge utile contient du code capable de modifier les packages npm locaux et de les publier sur
registry.npmjs.orgsans appeler la CLI npm. Si une machine de développeur disposait d’identifiants npm et a importélightning, considérez le jeton npm comme exposé et vérifiez les publications récentes des comptes associés à cette machine.Recherchez les dépôts de dépôt discret sur GitHub. Dans le cadre de la campagne plus large, l’exfiltration cible des dépôts publics appartenant aux comptes des victimes et présentant la description « A Mini Shai-Hulud has Appeared », ainsi que des messages de commit commençant par
OhNoWhatsGoingOnWithGitHub:. La requêtehttps://github.com/search?q=%22A+Mini+Shai-Hulud+has+Appeared%22&type=repositoriesaffiche les dépôts encore actifs.Vérifiez les résultats Snyk. Exécutez
snyk testsur les projets concernés ; l’avis SNYK-PYTHON-LIGHTNING-16323121 signale directement les deux versions malveillantes.
Ce que cela nous apprend sur le modèle de menace
Voici quelques observations tirées de cet incident, au-delà des mesures de nettoyage immédiates.
Les outils des attaquants sont réutilisés à un rythme plus rapide que celui auquel les registres peuvent retirer les packages compromis. Les versions malveillantes de lightning ont été détectées par analyse automatisée environ 18 minutes après leur publication, alors que le package était toujours installable et que le fil de divulgation sur GitHub avait été fermé. Le délai de 24 heures entre la campagne SAP CAP sur npm d’hier et la publication de lightning sur PyPI aujourd’hui concorde soit avec un opérateur unique qui passe d’un registre à l’autre, soit avec un imitateur qui réutilise la même charge utile tant qu’elle reste fonctionnelle.
La réutilisation de charges utiles entre écosystèmes au moyen de runtimes intégrés est un schéma récurrent. Snyk a couvert à deux reprises la semaine dernière des variantes pour npm combinant un chargeur Bun et une vaste charge utile obfusquée, et en observe maintenant une sur PyPI. La conclusion structurelle est que le vol d’identifiants, l’abus des dépôts GitHub et la logique d’auto-propagation npm sont les mêmes, quel que soit le registre ayant distribué le wheel ; traiter chaque compromission de registre comme un incident distinct fait passer à côté de la surface d’attaque commune après compromission.
Le mode de défaillance du processus de publication est désormais manifeste. Un jeton d’API PyPI à longue durée de vie, stocké dans les secrets GitHub Actions, sans liaison Trusted Publisher ni étape d’approbation manuelle, permet à un vol d’identifiants sur n’importe quel hôte de développeur ou de CI ayant accès à ce secret de compromettre un registre sans jamais toucher au workflow légitime. Cet incident, comme la compromission de SAP cap-js, met en évidence la même solution : une liaison OIDC Trusted Publisher, des identités distinctes pour l’administration des dépôts et la publication sur les registres, et une approbation humaine explicite avant la mise à disposition des versions sur le registre. L’analyse rétrospective de Snyk sur la résilience face à Shai-Hulud présente un ensemble plus large de mesures de contrôle. Snyk continue de suivre la filiation plus large de Shai-Hulud et les incidents connexes de la chaîne d’approvisionnement dans la Snyk Vulnerability Database ; l’avis en cours sur lightning sera mis à jour à mesure que la charge utile sera entièrement désobfusquée et que d’autres indicateurs de compromission seront confirmés.
Sécurisez vos applications Python dès maintenant
Détectez et corrigez gratuitement les vulnérabilités Python avec Snyk.
Aucune carte de crédit requise.
Ou inscrivez-vous avec Azure AD Docker ID Bitbucket
En utilisant Snyk, vous acceptez de respecter nos politiques, notamment nos Conditions d’utilisation et notre Politique de confidentialité.
