Lors du développement d'une application, un développeur va intégrer des dépendances tierces à son projet. Ces dépendances peuvent provenir de dépôts publics ou internes à l'entreprise. Les différents langages de programmation fournissent des gestionnaires de paquets pour faciliter l'installation et la mise à jour de ces dépendances (pip pour Python ou npm pour JavaScript par exemple). La confiance accordée à ces gestionnaires de paquets ouvre la voie à un nouveau type d'attaque permettant à un attaquant d'installer des dépendances malveillantes au sein du système d'information d'une entreprise. Cet article présente la possibilité de corrompre la chaîne d'approvisionnement (supply chain) d'un projet via ses dépendances privées, dont la sécurité est rarement remise en question.
Les attaques via la chaîne d’approvisionnement (supply chain en anglais) sont de plus en plus importantes, par exemple avec le cas de SolarWinds en 2020 : cette entreprise s'est fait pirater et les attaquants ont injecté une porte dérobée dans son système nommé « Orion ». Celui-ci est utilisé par plus de 30 000 entreprises, dont Microsoft et des entités gouvernementales, qui ont été piratées à leur tour via ce logiciel.
Ces attaques sont souvent critiques pour une entreprise, puisqu’elles permettent d’exécuter directement des commandes arbitraires à distance au cœur de son système d’information. Les attaquants contournent alors la plupart des protections périmétriques, et obtiennent le plus souvent des accès sur des serveurs critiques.
Les attaques sur les paquets dont dépendent les logiciels appartiennent à cette catégorie. C’est le cas par exemple du typosquatting, ou de la compromission du compte du développeur. Cet article vous propose de découvrir une technique révélée en 2021 par Alex Birsan [1] qui peut se révéler efficace même si des dépendances sont utilisées depuis un dépôt privé : la confusion de dépendances. Nous verrons également qu’un nouveau type de risque commence à émerger : le sabotage d’un paquet par son propre développeur.
1. Origine de l’idée
Un fichier package.json contient, entre autres, la liste des paquets utilisés par une application Node.js :
Alex Birsan, qui est un bug bounty hunter, en a trouvé un avec un collègue qui appartenait à PayPal et accessible sur GitHub. Il contenait des paquets publics et d’autres inexistants sur les dépôts npm publics : ces derniers sont donc probablement privés et hébergés en interne chez PayPal. Comme tous les paquets sont dans le même fichier, plusieurs questions se posent :
- Quel est l’ordre de recherche du gestionnaire de paquets ? Que se passe-t-il si l’on crée des paquets malveillants sur les dépôts officiels npm avec le nom des paquets privés ?
- Est-ce que ce code va être exécuté automatiquement sur les postes des développeurs ou sur des serveurs de l’entreprise ciblée ?
- Peut-on étendre cette attaque à d’autres entreprises ou à d’autres gestionnaires de paquets ?
2. Tests
Le chercheur, avec l’autorisation des entreprises ciblées, a mis en place la stratégie suivante pour répondre à ces questions :
- Utiliser la directive preinstall dans son paquet malveillant, pour que la commande npm exécute son code arbitraire à chaque installation même si le build échoue ;
- Faire de l’exfiltration de données via DNS, avec seulement le nom d’utilisateur, le nom de la machine et le chemin d’installation. Cette méthode a été choisie, car les serveurs touchés pouvant être au cœur de réseaux d’entreprises, elle a la plus grande probabilité de traverser les multiples couches de protection qui pourraient bloquer le trafic sortant ;
- Adapter cette technique à d’autres langages comme Python et Ruby, pour créer des paquets sur PyPI (Python Package Index) et RubyGems ;
- Trouver le plus grand nombre de noms de paquets privés possible.
Cette dernière partie est la plus importante et aussi la plus difficile. Quelques jours de recherches lui ont permis d’en trouver sur GitHub, sur des dépôts officiels (où des paquets privés avaient été accidentellement publiés) et sur des forums.
Cependant, les fichiers JavaScript furent de loin la meilleure source pour ces noms. Il est en effet courant que des fichiers package.json internes (contenant les noms des dépendances) se retrouvent intégrés dans les scripts publics durant le processus de build. Le résultat est présenté en figure 1.
Cela fut le cas pour Apple, Yelp et Tesla par exemple. Avec l’aide d’un autre chercheur, il a ainsi pu récupérer plusieurs centaines de noms de paquets privés JavaScript en scannant des sites web publics.
Il a ensuite enregistré des paquets malveillants pour tous les noms qu’il avait trouvés sur les différents dépôts publics, en utilisant des numéros de version supérieurs à ceux trouvés pour augmenter leurs chances de téléchargement. Puis il a attendu les retours de ses requêtes DNS. Un schéma de l’attaque est présenté en figure 2.
3. Résultats
Le taux de réussite a été au-delà de ses attentes : il a pu détecter la faille chez plus de 35 entreprises dans les 3 langages testés (Python, Ruby et JavaScript). La plupart des entreprises vulnérables avaient plus de 1 000 employés : cela est logique puisque ce sont elles qui ont le plus de moyens pour développer des paquets en interne. Des serveurs internes comme des postes de développeurs ont été touchés, avec des délais de compromission de quelques heures ou parfois de quelques minutes après la création des paquets malveillants. Cela montre à quel point cette attaque peut être efficace pour pénétrer le réseau interne d’une entreprise. C’est d’autant plus critique que les serveurs de build ou les postes de développeurs contiennent des secrets permettant d’accéder à des éléments sensibles d’une entreprise, la plupart du temps avec des privilèges élevés : serveurs de production, dépôts de code privés, bases de données, etc.
4. Analyse de la vulnérabilité
La vulnérabilité réside dans plusieurs choix d’implémentation standards : pris séparément, ils semblent anodins et raisonnables, mais combinés ensemble un attaquant peut en abuser.
Tout d’abord, tout le monde peut créer un paquet sur un dépôt public tant que le nom est libre. Cela facilite le partage de paquets, mais permet à n’importe qui d’en créer un qui n’existe pas.
Ensuite, les noms des paquets privés et publics ne sont jamais comparés. Les paquets privés n’étant pas publiés par définition, leurs noms sont libres sur les dépôts publics tant que personne ne les crée.
Enfin, la plupart des gestionnaires de paquets permettent d’ajouter des dépôts (éventuellement privés) en plus des dépôts publics, et le processus de décision est alors le suivant :
- Vérifier si le paquet existe sur la liste de dépôts (éventuellement privés) donnée.
- Vérifier si le paquet existe sur les dépôts publics.
- Installer le paquet trouvé. S’il existe sur plusieurs dépôts, la configuration par défaut installe celui avec le plus grand numéro de version.
En conclusion : « it’s not a bug, it’s a feature ». Cette dernière fonctionnalité qui privilégie les paquets les plus à jour est généralement souhaitée. Heureusement, la plupart des gestionnaires de paquets permettent de mieux gérer ce comportement par défaut, ou ont implémenté des corrections le permettant (contrairement à d’autres entreprises utilisant cet adage pour ne pas corriger leurs failles). Nous allons voir plus en détail des remédiations possibles pour certains langages et gestionnaires de paquets.
5. Remédiations
Il est à noter que malheureusement, beaucoup d’organisations compromises par le chercheur n’ont pas partagé de détails concernant leurs remédiations ou la cause principale de cette vulnérabilité dans leur configuration spécifique. La diversité des configurations possibles est immense, aussi le lecteur est invité à adapter et modifier les recommandations génériques suivantes en fonction des spécificités de son environnement.
De plus, un certain nombre de recommandations se base sur l’utilisation d’un unique dépôt privé comme proxy. Il faut impérativement s’assurer que celui-ci ne télécharge pas des versions plus récentes des paquets privés sur des dépôts publics, sinon l’attaque reste toujours possible.
Enfin, en plus des remédiations spécifiques à un langage ou à un gestionnaire de paquets mentionnées ci-dessous, il est possible d’utiliser des versions précises des paquets (ex : 1.2.3) au lieu d’intervalles comme ≥ 1.2.3 ou 1.2.*. Cela évite les mises à jour forcées basées sur des numéros de version. Il faudra cependant s’assurer d’avoir un processus de montée de version régulier pour avoir les derniers correctifs de sécurité, puisque les mises à jour ne seront plus automatiques. Les versions devront être changées manuellement dans le fichier de configuration.
5.1 Diminution globale de l’impact
Pour limiter l’impact de la vulnérabilité en cas d’attaque réussie, il est possible de séparer les deux types de serveurs :
- ceux de build, vulnérables aux attaques par chaîne d’approvisionnement ;
- ceux déployant le résultat sur les serveurs d’exécution, qui contiennent les secrets les plus sensibles (pour se connecter aux autres serveurs et installer les applications).
De cette manière, un attaquant ne peut pas accéder facilement aux différents secrets.
Il est également possible d’avoir des machines de build en dehors du système d’information de l’entreprise, afin qu’une attaque réussie ne l’atteigne pas. Ainsi, un attaquant n’obtient pas directement un compte sur le domaine Active Directory.
5.2 npm
Npm ayant le comportement par défaut d’aller chercher les paquets dans le dépôt public npmjs, il est d’abord essentiel de s’assurer qu’il possède des informations valides de connexion aux dépôts privés : soit via l’invite de commande, soit via un fichier .npmrc ou soit via la commande npm config set registry. L’absence de ces informations peut notamment se produire lors de l’installation d’un poste d’un nouveau développeur, ou lors de la configuration d’un nouveau serveur.
Ensuite beaucoup de dépôts privés vont également vérifier s’il existe une version plus récente sur le dépôt public, et la télécharger si elle existe. Selon la configuration, il peut cependant y avoir une vérification sur la date de publication : si une version avec une version mineure plus petite a été publiée après un paquet avec une version mineure supérieure, alors le paquet public n’est pas téléchargé. Il vaut mieux néanmoins correctement configurer les dépôts privés et proxies pour qu’ils ne fassent jamais de requêtes sur des dépôts non autorisés.
Par ailleurs, mettre à jour manuellement via npm update, npm install <packages>@latest ou yarn upgrade aboutira également à une recherche sur le dépôt public même si un proxy est configuré correctement dans .npmrc ! Il est préférable d’utiliser à la place des mises à jour automatiques via des pull requests faites sur les dépôts privés [2].
Le moyen le plus fiable et sécurisé reste d’utiliser les périmètres (scope) npm : ils permettent de regrouper les paquets par organisation et/ou utilisateur. Chacun ayant son propre périmètre et étant le seul à pouvoir publier des paquets dessus, il n’y a plus de risque de confusion ou de collision. Cela oblige cependant à renommer les paquets existants qui ne les utiliseraient pas.
Enfin, l’outil open source snync développé par snyk.io permet de détecter si un projet ou un paquet est vulnérable en vérifiant sa présence sur le dépôt officiel. Il détecte aussi certains cas suspects [3].
5.3 Python
Pour Python, l’usage de l’option --extra-index-url, couramment utilisée pour ajouter d’autres dépôts, introduit cette vulnérabilité : cela ajoute des dépôts à vérifier, mais c’est le paquet le plus à jour qui est téléchargé. L’utilisation de --index-url à la place (ou -i) permet de remplacer le dépôt officiel par un autre dépôt (éventuellement privé). Le dépôt officiel n’étant plus vérifié, cela corrige la vulnérabilité.
5.4 Ruby
Pour Ruby, de manière similaire l’usage de --source introduit la vulnérabilité. Il faut rajouter l’option --clear-sources juste avant afin de supprimer tous les dépôts, incluant le dépôt officiel. Seul le dépôt privé sera alors vérifié.
5.5 Jfrog Artifactory
Cet outil se veut universel et supporte tous les langages. Il permet d’avoir une gestion centralisée de ses artefacts.
Il propose par défaut de mélanger des dépôts publics et internes au sein de dépôts « virtuels ». Cela engendre la vulnérabilité présentée ici. Il possède depuis de nombreuses années la possibilité d’utiliser des modèles d’exclusion pour ses dépôts, et il recommande fortement de les utiliser pour que les paquets privés ne soient pas cherchés sur les dépôts publics. Cependant, si les paquets privés ne suivent pas une nomenclature simple, le nombre de règles peut rapidement devenir important et leur rédaction être fastidieuse. Ainsi, face aux nombreuses remontées de ses utilisateurs à la suite de la publication de cette vulnérabilité, l’éditeur a ajouté la possibilité de configurer une priorité de résolution au niveau des dépôts [4]. Cependant, dans le cas où les dépôts prioritaires ne contiendraient pas le paquet (privé) recherché (par exemple en cas de coupure réseau ou autre problème de disponibilité), la recherche se fera alors sur le dépôt public. Pour cette raison, Jfrog recommande toujours d’utiliser leurs modèles d’exclusion sur un dépôt virtuel contenant tous les dépôts publics pour se prémunir contre cette vulnérabilité.
5.6 Sonatype Nexus
Il est possible de définir des règles de routage pour les dépôts en amont. Elles sont basées sur des expressions régulières sur les URI demandées qui les bloquent ou les autorisent selon la règle [5]. Là aussi, il faut que la nomenclature des paquets privés permette de les identifier simplement sous peine d’être obligé d’écrire de très nombreuses règles. Cela peut conduire dans des cas extrêmes à du déni de service sur les expressions régulières, qui peuvent être très gourmandes en puissance de calcul.
5.7 NuGet Gallery
Il est possible de configurer un proxy : il faut démarrer la section packageSources du fichier nuget.config avec une balise <clear /> pour s’assurer de supprimer toute source héritée. Puis il faut ajouter une seule balise <add /> pour ajouter le dépôt privé.
Il est également possible d’enregistrer un identifiant de préfix sur nuget.org pour limiter les publications à des comptes autorisés [6]. Ce mécanisme est similaire aux périmètres de paquets sur npm.
5.8 Maven
Il faut configurer un miroir unique qui est <mirrorOf>*</mirrorOf> pour rediriger toutes les requêtes vers un dépôt unique qui réalise ses propres redirections.
Les paquets publiés sur Maven Central le sont seulement sur un espace de nom contrôlé par le propriétaire et vérifié par DNS. Si les paquets privés sont nommés en se basant sur ce nom de domaine, ils sont alors protégés contre cette attaque. Il faut veiller à toujours contrôler le nom de domaine, sous peine de voir un attaquant opportuniste en prendre le contrôle (à l’expiration de son enregistrement par exemple).
6. Détection
La plupart des attaques ne vont pas générer d’erreurs d’installation. C’est au build que l’erreur va se produire si l’attaquant a seulement eu accès au nom du paquet et non à ses sources. Donc des échecs inexpliqués de build ou de tests peuvent être des signes de cette attaque méritant investigation.
Il est important de noter que sur certaines plateformes, un attaquant peut supprimer son paquet. Donc il peut y avoir un seul build qui échoue suivi d’autres terminés avec succès, mais cela suffit à un attaquant bien préparé pour exécuter son code et compromettre le serveur/le poste du développeur. Cela rend l’attaque d’autant plus difficile à détecter.
L’utilisation d’un proxy interne facilitera l’analyse et la détection, car tous les paquets installés restent dans le cache même après suppression dans le dépôt en amont. Cela est encore plus simple si le proxy permet de voir la source originale pour chaque version d’un paquet : l’historique montrera plusieurs versions du paquet depuis le dépôt privé, puis des versions récentes venant d’un dépôt public.
7. Auto-sabotage
La situation est déjà complexe pour bien sécuriser et gérer sa chaîne d’approvisionnement de paquets, comme nous venons de le voir.
Les entreprises sont conscientes d’autres risques, par exemple la compromission du compte du développeur. Ce cas devient heureusement assez rare, avec notamment la généralisation du MFA pour ces comptes. Ce qui est heureusement encore plus rare, mais plus inquiétant, est le sabotage d’un paquet par son développeur lui-même.
Le premier exemple est celui des 2 bibliothèques Faker.js et Colors.js développées par Marak Squires. Ces bibliothèques sont utilisées par plus de 21 000 projets, avec plus de 22 millions de téléchargements par semaine. Début janvier, il a introduit du code qui contenait une boucle infinie précédée du texte « Liberty Liberty Liberty ». Cela a conduit au dysfonctionnement de toutes les applications qui en dépendaient, engendrant une certaine panique chez beaucoup de développeurs. Les motivations du développeur sont assez floues : en novembre 2021, il avait laissé un message contre les grandes entreprises qui ne rendent rien à la communauté, et son dernier readme indique : « What really happened with Aaron Swartz? ». Aaron Swartz était un hacktiviste américain dont Squires contredit la théorie du suicide.
Le second exemple est celui du paquet node-ipc. Il est utilisé par plus de 350 projets dont l’invite de commande de Vue.js, et est téléchargé plus de 4 millions de fois par mois. Son développeur RIAEvangelist a ajouté du code malveillant début mars 2022 : il écrase le contenu de tous les fichiers du poste de travail/serveur l’exécutant par l’émoji cœur « ❤️ » si son IP est géolocalisée en Russie ou en Biélorussie (le déroulé exact des évènements est disponible ici [7]). Ce code destructeur est resté en ligne moins de 24h, mais a malgré tout fait des victimes. La motivation ici était de protester contre la guerre en Ukraine, et d’affecter des utilisateurs supposés pro-guerre. Malheureusement, cela cible tout le monde, puisqu’une IP ne révèle en rien les sensibilités politiques des personnes.
D’autres développeurs ont suivi ce mouvement de protestation contre la guerre en Ukraine, avec des impacts plus limités et des protestations non destructrices, principalement des messages demandant la fin des hostilités.
En première lecture, il est très inquiétant que le bon fonctionnement des applications d’une entreprise puisse dépendre de l’état d’esprit d’un développeur externe, dont l’humeur peut changer du jour au lendemain. Les motivations importent peu pour le risque, puisqu’on peut toujours être l’ennemi de quelqu’un, ou bien simple victime collatérale. Cependant, il est important de souligner que dans l’immense paysage de l’écosystème open source, ces cas font figure d’exceptions très rares. La question se pose de savoir si ce genre de comportement va avoir tendance à se développer, ou si la communauté parviendra à rester telle que nous la connaissons : très majoritairement fiable et non malveillante. La réputation restant un critère important dans notre secteur, il est probable que les personnes commettant ces actions ne pourront le faire qu’une seule fois, et que cela aura un impact très important sur leur carrière. De nombreuses personnes de la communauté (y compris favorables aux revendications) ont estimé qu’une limite inacceptable avait été franchie. D’autant plus que cela affecte tout l’écosystème open source, basé sur la confiance, et pas seulement les deux développeurs mentionnés.
Des développeurs de la communauté open source ont décidé de proposer un outil pour permettre une première protection face à cette menace sur les paquets JavaScript. Il s’agit de l’outil Socket [8]. Il est gratuit pour les dépôts publics et en passant par leur site. Il réalise de l’analyse poussée du code source des paquets et de leurs dépendances. Il tente de détecter les changements potentiellement malveillants dans le code : ajout de communication réseau, code masqué, appels aux commandes systèmes, etc. Étant développé spécifiquement pour les paquets, il sera plus efficace que les outils génériques d’analyse de code. Il est encore jeune et vient de sortir de bêta : il faudra vérifier s’il est efficace sur le long terme ou s’il existe des contournements comme dans le cas des pare-feux applicatifs. À défaut d’être 100 % efficace, il pourra constituer une première ligne défensive intéressante.
En effet, la relecture manuelle de toutes les modifications pour chaque changement de version de chaque paquet dont elle dépend n’est pas réaliste pour une entreprise. Le facteur humain du développeur est hors de portée. Néanmoins une option pourrait améliorer la situation. Elle nécessite une collaboration entre les entreprises qui pourrait leur profiter à toutes, sur le modèle du partage des indicateurs de compromission (IOC) entre les centres de sécurité opérationnels (SOC) : chacun fait les analyses dont il a besoin de son côté, mais partage certaines informations à sa disposition avec les autres entreprises pour qu’elles se prémunissent du danger. Là, il s’agirait de mutualiser l’effort d’analyse des modifications de certains paquets et/ou de partager son résultat. Les entreprises se concentreraient alors sur ceux très fortement téléchargés et gérés par un seul individu : publier du code malveillant à l’insu des utilisateurs étant un acte très fort, il est probable que dans un groupe au moins une personne soit totalement contre et donne l’alerte. Les entreprises pourraient ainsi profiter des analyses des autres ou se répartir les relectures. Cependant, vu l’extrême diversité des différents écosystèmes et langages, cela peut se révéler ardu à mettre en pratique. Peut-être qu’un service payant verra le jour et répondra à ce besoin d’analyse manuelle des principaux paquets, s’il devient suffisamment important et pertinent.
Conclusion
Nous avons vu comment se protéger des attaques par confusion de dépendances. Cette vulnérabilité permet de se souvenir à quel point il est important de maîtriser les outils utilisés, de connaître leurs options et comportements pour éviter des scénarios non souhaités. Cette faille n’a pas requis de compétences techniques très pointues pour être découverte, simplement la bonne compréhension d’éléments connus, et le détournement de leur usage normal. Elle est pourtant redoutablement efficace ! Il est probable que d’autres failles fonctionnelles similaires existent parmi la multitude de logiciels utilisés par les entreprises. La complexification du système d’information ne doit pas faire perdre de vue sa nécessaire maîtrise.
Il est très difficile de se prémunir de l’auto-sabotage par les développeurs, mais cela restera probablement un cas à la marge. Cependant, cela invite à se poser de nouveau ces questions : à qui faisons-nous confiance ? Avons-nous fait les vérifications préalables avant d’intégrer ce logiciel dans notre système d’information ? L’avons-nous correctement configuré ?
Enfin, participer et s’investir dans la communauté open source permettra de s’assurer qu’elle garde son cercle vertueux dont nous bénéficions tous.
Remerciements
Je tiens à remercier chaleureusement l’équipe de RandoriSec de m’avoir encouragé à la rédaction de cet article, et de m’avoir donné les moyens de le réaliser.
Références
[1] Article original du chercheur Alex Birsan :
https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
[2] Article discutant des différentes problématiques de cette attaque avec npm :
https://snyk.io/blog/detect-prevent-dependency-confusion-attacks-npm-supply-chain-security/
[3] Outil permettant de détecter les paquets JavaScript vulnérables avec npm :
https://GitHub.com/snyk-labs/snync
[4] Article de Jfrog Artifactory sur la remédiation pour cette vulnérabilité :
https://jfrog.com/blog/going-beyond-exclude-patterns-safe-repositories-with-priority-resolution/
[5] Détail de la configuration d’une règle de routage pour Sonatype :
https://help.sonatype.com/repomanager3/nexus-repository-administration/repository-management/routing-rules
[6] Réserver un préfix sur nuget.org : https://docs.microsoft.com/en-us/nuget/nuget-org/id-prefix-reservation
[7] Détails des faits pour le sabotage de node-ipc :
https://snyk.io/blog/peacenotwar-malicious-npm-node-ipc-package-vulnerability/
[8] Outil Socket pour analyser le changement de code des dépendances en JavaScript : https://socket.dev/