On s’imagine souvent que les attaques supply-chain, c’est un truc « enterprise », loin de nos projets React. Dans la vraie vie, ça tape exactement là où on ne regarde pas : une dépendance qui a l’air normale, un nom qui ressemble au bon, une version qui inspire confiance… et tu viens d’exécuter du code d’un inconnu sur ta machine.
Le cas qui m’a marqué récemment, c’est un package nommé react-refresh-update, pensé pour ressembler à l’écosystème react-refresh (Fast Refresh). Je m’en sers ici comme point de départ pour un playbook simple, actionnable, et surtout reproductible. Pas pour psychoter. Pour arrêter d’installer « à l’instinct ».
Le cas « react-refresh-update » : le piège est justement qu’il a l’air crédible
Ce genre d’histoire fonctionne parce qu’elle coche les cases du quotidien : tu es en train de bricoler ton tooling (Vite, Next, un plugin, un fix HMR), tu vois passer un nom proche de ce que tu connais, et tu te dis « ok, ça doit être un petit wrapper ». Ça ressemble à un package utilitaire. Ça ne ressemble pas à un malware.
D’après l’analyse publiée par SafeDep, le package s’appuyait sur plusieurs techniques classiques de tromperie : un nom très proche d’un composant connu, une version “normale” (voire un peu trop flatteuse), et un code qui ne crie pas « je suis un trojan » au premier coup d’œil. Le détail qui fait mal, c’est que tu n’as pas besoin de le déployer en prod pour être impacté. Il suffit de l’installer. Sur ton poste. Là où sont tes tokens, tes clés SSH, tes navigateurs, tes sessions.
Le takeaway est simple : la menace n’est pas « npm est dangereux ». La menace, c’est notre façon de consommer npm quand on est pressé.
Sponsorisé par Le Scribouillard
Besoin de contenu optimisé SEO ?
Utilisez la meilleure plateforme française de création de contenu assistée par IA ! Et générez des articles pour moins de 1€ !
Les signaux faibles que je veux voir avant même d’installer
Je pars d’un principe : une dépendance qui va s’exécuter sur mon poste doit mériter un minimum de friction. Pas 2 heures d’audit, mais au moins 2 minutes de bon sens. Et oui, c’est plus important pour les packages liés au build, au dev server, au lint, aux loaders. C’est eux qui tournent avec les droits de ta machine.
Premier signal qui me fait lever un sourcil : un nom “presque pareil” (typosquatting) ou un nom qui donne l’impression d’être officiel alors qu’il ne l’est pas. Dans le cas de react-refresh, l’écosystème a déjà des packages identifiés. Quand je tombe sur un « update », « pro », « fix », « lite », je considère que c’est à moi de prouver que c’est légitime, pas l’inverse.
Deuxième signal : le timing et l’historique. Un package très récent, sans historique de releases, sans issues, sans README consistant, ou avec une activité bizarre (un seul commit, un seul fichier modifié, zéro discussion) mérite une pause. Ce n’est pas une preuve de malveillance, mais c’est un profil à risque. Les bons outils ont des gens autour. Les outils “jetables” ont souvent… personne.
Troisième signal : un versioning qui sent l’arnaque. Une version « trop belle pour être vraie », une 2.x qui arrive sans histoire, une montée de version qui ne colle pas à la maturité du repo, ou un écart étrange avec le package “référence”. C’est le genre de détail qui ne coûte rien à falsifier, et qui marche très bien sur nos biais.
Quatrième signal : un publisher jetable. Si l’identité du mainteneur ressemble à un compte créé hier, sans autres packages solides, sans présence, sans liens clairs, je ralentis. Là aussi, ce n’est pas un tribunal, c’est de l’hygiène. Le but est de réduire la surface d’attaque, pas d’avoir raison sur Twitter.
Le vrai garde-fou : arrêter d’installer « en freestyle »
Le meilleur moment pour se protéger, ce n’est pas après un incident. C’est quand tu ajoutes une dépendance. Et la base, c’est bête : verrouiller ce qui est installé, et rendre visibles les changements.
Si ton projet n’est pas strict sur son lockfile, tu joues à la roulette. Le lockfile n’est pas un détail, c’est le contrat. Dans une équipe, je veux que le lockfile soit versionné, relu comme du code, et que ses diffs soient compréhensibles. Si un simple ajout de package déclenche un remaniement massif du lock, je considère que c’est un événement. Pas « npm qui fait sa vie ».
Ensuite, je me méfie particulièrement des scripts d’installation (postinstall, preinstall…). Ce n’est pas “mal” en soi, beaucoup d’outils compilent des binaires, téléchargent des artefacts, génèrent des fichiers. Mais c’est exactement le point d’entrée rêvé pour exécuter quelque chose en douce. Donc si un package n’a aucune raison d’exécuter du code à l’install, et qu’il en exécute, je considère que j’ai une explication à obtenir.
Enfin, je préfère des modes d’installation déterministes. Sur npm, ça veut dire npm ci en CI (et souvent même en local quand je veux reproduire un environnement). Sur pnpm ou Yarn, je veux l’équivalent « immutable/frozen ». L’idée est la même : si le lockfile ne correspond pas, l’install échoue. Ça évite les “petites variations” qui deviennent des gros incidents.
En CI, je veux du déterminisme, pas de la chance
La CI, c’est l’endroit où tu peux imposer des règles sans négociation. Et franchement, si tu laisses ta CI faire un npm install permissif, tu prends un risque évitable. L’objectif n’est pas de créer une forteresse impraticable, c’est de rendre les surprises difficiles.
Concrètement, je veux une CI qui installe uniquement ce qui est dans le lockfile, qui utilise une version de Node/pnpm/yarn contrôlée (via une stratégie de versions et, quand c’est pertinent, via Corepack), et qui ne cache pas des comportements étranges derrière un cache opaque. Les caches sont utiles, mais en incident, ils deviennent un piège. Si tu ne sais pas purger proprement, tu ne sais pas diagnostiquer.
Et si ton registre le permet, je m’intéresse de plus en plus aux signaux de provenance et de signature. Ce n’est pas magique, ce n’est pas universel, mais c’est une couche de défense de plus : réduire la probabilité qu’un artefact ait été publié “n’importe comment” et rendre les attaques plus coûteuses.
# Exemple minimal GitHub Actions : install déterministe + audit (à adapter à ton projet)
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm audit --audit-level=high
- run: npm testOui, npm audit n’attrape pas tout (et certainement pas tous les malwares). Mais ça attrape une partie du bruit, ça impose une discipline, et surtout ça t’habitue à traiter la dépendance comme un sujet d’ingénierie, pas comme un détail.
Inspecter un package en 2 minutes (sans jouer à l’analyste malware)
Quand un package me paraît louche, je n’essaie pas d’être plus malin que les attaquants. Je cherche juste des indices grossiers : est-ce qu’il y a des scripts d’install, est-ce qu’il y a du code obfusqué, est-ce qu’il télécharge des binaires, est-ce qu’il touche au réseau, est-ce qu’il écrit dans des endroits bizarres.
Le moyen le plus simple, c’est de récupérer le tarball et de regarder ce qu’il contient. Ça ne remplace pas une analyse, mais ça filtre déjà pas mal de “mauvais plans”. Et ça te donne un réflexe : tu regardes avant d’exécuter.
# 1) Voir les infos basiques (auteur, dist-tags, scripts, dépendances…)
npm view react-refresh-update --json
# 2) Récupérer le tarball exact publié sur le registry
npm pack react-refresh-update
# 3) Lister le contenu sans exécuter quoi que ce soit
tar -tf react-refresh-update-*.tgz | sed -n '1,120p'
# 4) Chercher rapidement des signaux classiques (obfuscation, download, postinstall)
tar -xOf react-refresh-update-*.tgz package/package.json | cat
tar -xOf react-refresh-update-*.tgz package/package.json | grep -E "postinstall|preinstall|install" -n
Je ne suis pas en train de dire « si tu vois postinstall, c’est malveillant ». Je dis : si tu vois postinstall, tu dois savoir pourquoi il est là. Et si tu vois du code compressé/obfusqué dans un package qui n’a aucune raison de l’être, tu as assez d’éléments pour ne pas l’installer “pour tester”.
Les erreurs que je vois tout le temps (et qui font rentrer le loup)
La première, c’est l’installation « juste pour voir » sur la machine de boulot, celle qui a accès à tout. Si tu veux tester un package douteux, fais-le dans un environnement jetable. Une VM, un conteneur, un repo sandbox. Pas sur ton poste avec tes credentials.
La deuxième, c’est de laisser les mises à jour de dépendances se faire sans regard humain. Automatiser oui. Autopiloter non. Un bot qui ouvre une PR de bump, c’est bien. Mais la review doit exister, et pas seulement sur le code applicatif. Le lockfile fait partie du changement.
La troisième, c’est la confiance aveugle dans « c’est populaire donc c’est safe ». La popularité réduit certains risques, pas tous. Et les attaquants adorent les packages populaires… ou les noms qui ressemblent à des packages populaires.
Si tu l’as installé : un protocole d’incident simple (et pas héroïque)
Si tu réalises qu’un package potentiellement malveillant a été installé, ne te perds pas dans des micro-analyses. Le plus important est de limiter l’impact.
Je commence par isoler : couper les processus liés au projet, éviter de relancer l’IDE et les watchers, et si je suis en environnement pro, prévenir rapidement quelqu’un côté sécurité. Ensuite je pars sur l’hypothèse réaliste : des secrets ont pu fuiter. Donc rotation des tokens (npm, GitHub, GitLab, cloud), régénération des clés si nécessaire, invalidation des sessions, et vérification des accès récents. Ça fait mal, c’est pénible, mais c’est souvent moins cher que de « surveiller en espérant ».
Après ça, je nettoie proprement : suppression du node_modules, purge des caches de package manager si tu soupçonnes une contamination, réinstallation via une commande déterministe (type npm ci) et à partir d’un lockfile sain. Si tu dois investiguer, tu le fais sur une copie, pas en continuant à travailler dans l’arbre potentiellement compromis.
Et dernier point qui sauve du temps la prochaine fois : documenter le minimum. Quel package, quelle version, quand il a été introduit, par quelle PR, quels environnements l’ont installé (poste dev, CI, builders). Tu veux pouvoir répondre en 10 minutes à « qui est touché ? », pas improviser au milieu de la panique.
Conclusion : le bon réflexe, c’est de rendre l’attaque coûteuse
Tu n’empêcheras pas npm d’être un terrain de chasse. Par contre, tu peux arrêter d’être une cible facile. Un lockfile strict, une CI déterministe, deux minutes d’inspection quand un nom te semble bizarre, et un protocole d’incident clair… ça ne rend pas invincible. Ça rend juste l’erreur moins probable, et l’impact plus maîtrisable.
Et surtout, ça remet la dépendance à sa place : ce n’est pas un « détail de tooling ». C’est du code tiers qui s’exécute chez toi. Donc oui, ça mérite un vrai process.