Après TanStack : cessez de traiter npm comme un CDN — un plan de CTO pour la chaîne d’approvisionnement JavaScript

Par Diogo Hudson Dias
CTO reviewing a private npm registry dashboard on a large monitor in a São Paulo office while engineers discuss security.

Vous ne contrôlez pas ce qui s’exécute quand vous tapez npm install. La compromission npm de TanStack l’a montré de façon douloureuse. Si l’un des mainteneurs JavaScript les plus fiables peut voir son canal de distribution détourné, votre pipeline le peut aussi. Traiter npm comme un CDN est une faute organisationnelle.

Ce billet est un cadre de décision et un plan de déploiement. Il emprunte son urgence à deux gros titres voisins : l’incident de chaîne d’approvisionnement de TanStack, et le rapport de Google selon lequel des groupes criminels utilisent déjà l’IA pour trouver des failles logicielles majeures plus vite qu’avant (lien). En combinant ces réalités, la probabilité d’exécuter à votre insu un script postinstall malveillant cette année n’est plus théorique.

Les chiffres qui dérangent des chaînes d’approvisionnement JavaScript

Le registre npm héberge bien plus de deux millions de packages. Un front-end moderne ou un service Node référence des dizaines de dépendances directes et des centaines — souvent plus de mille — transitives. Beaucoup embarquent des scripts de cycle de vie qui s’exécutent lors de l’installation. Votre CI/CD, les laptops de vos développeurs et vos environnements éphémères de prévisualisation exécutent ce code automatiquement. C’est de l’exécution de code à distance par défaut.

Si un compte de mainteneur populaire se fait phisher, si un jeton OAuth fuit, ou si un workflow de build est compromis, des versions malveillantes peuvent atterrir dans votre graphe de dépendances en quelques minutes. Et comme les développeurs autorisent souvent les mises à jour mineures et de correctifs, la dérive silencieuse est courante. C’est ainsi que les typosquats, la dependency confusion et les prises de contrôle de comptes se transforment en incidents à l’échelle de l’organisation.

Cadre de décision pour CTO : trois couches de défense

Vous ne pouvez pas éliminer le risque, mais vous pouvez changer les modes de défaillance. Votre objectif est d’empêcher du code inconnu d’atteindre votre flotte sans contrôle, et de minimiser le rayon d’explosion lorsqu’il y parvient.

Couche 1 : Politique et provenance (décider de ce qui peut entrer)

  • Registre privé et curaté comme unique source. Placez un proxy devant npm et faites-en la source de vérité unique. Options : Verdaccio pour les équipes qui veulent un cache léger, ou des registres d’entreprise comme JFrog Artifactory, Sonatype Nexus ou AWS CodeArtifact. Bloquez l’accès direct à registry.npmjs.org depuis la CI et les réseaux de dev.
  • Liste d’autorisation de packages et éditeurs figés. Traitez votre registre comme un pare-feu de packages. Ne mirrorez que les packages approuvés, et, si besoin, figez les identités des éditeurs. Si un package change de mainteneur ou est dépublié puis republé, exigez une revue humaine.
  • Imposez des lockfiles immuables et des installations déterministes. Utilisez npm ci (pas npm install) dans la CI. Verrouillez sur des versions exactes et commitez le lockfile. Avec pnpm ou Yarn, forcez les modes de lockfile figé. Cela suffit déjà à empêcher les mises à jour surprises d’atterrir pendant les builds.
  • Exigez la provenance de build pour les packages critiques. GitHub prend en charge la provenance des packages npm (basée sur Sigstore) qui relie un package publié à un dépôt et un workflow CI. Préférez, pour tout ce qui est critique, les packages publiés avec provenance.
  • Namespacez vos packages internes pour éliminer la dependency confusion. Utilisez un scope privé @org et configurez npm pour ne le résoudre que depuis votre registre. Ne publiez jamais de noms internes sur le registre public npm. Définissez explicitement le mapping du registre : npm config set @yourorg:registry https://your-registry.example.com/

Couche 2 : Contrôles de pipeline (contrôler comment ça entre)

  • Désactivez par défaut les scripts de cycle de vie en CI. Installez avec npm ci --ignore-scripts et exécutez une phase de scripts minimale et revue si nécessaire. La plupart des packages n’ont pas besoin de postinstall ; ceux qui en ont besoin doivent être des exceptions avec des règles d’autorisation explicites.
  • Contrôle des sorties réseau en CI. N’autorisez que les connexions sortantes vers votre registre privé et vos stores d’artifacts. Bloquez l’accès Internet pour les étapes de build qui touchent aux dépendances. Si votre runner cloud ne sait pas faire, migrez vers un runner qui peut ou auto-hébergez.
  • Analyse statique et comportementale à l’import. Utilisez plusieurs scanners : vulnérabilités classiques (npm audit, Snyk), maintenabilité et comportement (Socket, Phylum), et hygiène de projet (OpenSSF Scorecards). Faites échouer les builds pour les packages qui gagnent soudain des scripts d’installation, du code obfusqué, ou des opérations réseau/fichier risquées.
  • Les SBOM sont le minimum vital. Générez une SBOM à chaque build (CycloneDX ou SPDX). Stockez-la avec votre artifact. C’est ainsi que vous répondez en quelques minutes, et non en jours, à la question « Sommes-nous affectés ? » lorsque le prochain incident tombe.
  • Builds reproductibles. Containerisez les builds avec des images de base figées, des versions figées du gestionnaire de paquets (utilisez Corepack pour figer npm/yarn/pnpm), et aucun état ambiant. Si vous rebuildez le même commit dans un environnement propre et obtenez un artifact différent, vous avez un problème de chaîne d’approvisionnement.

Couche 3 : Contrôles d’impact à l’exécution (en supposant que ça passe)

  • Modèle de permissions de Node. Les versions récentes de Node incluent un modèle de permissions optionnel qui peut refuser fs et net par défaut. Encapsulez vos processus avec l’ensemble minimal de chemins et d’hôtes autorisés. Cela transforme un implant de supply chain réussi en un no-op bruyant et bloqué.
  • Containerisation et sandboxing des appels système. Exécutez les services Node sous des profils seccomp/apparmor ou gVisor. Pour les jobs de build front-end, utilisez des conteneurs non privilégiés sans socket docker. Un postinstall malveillant ne doit pas pouvoir docker pull, ssh, ou curl des hôtes arbitraires.
  • Listes d’autorisation de sortie à l’exécution. Même si le code s’exécute, restreignez le trafic sortant à ce dont le service a strictement besoin. Un beacon vers un hôte de commande et contrôle devient un échec DNS bloqué sur lequel vous pouvez alerter.
  • Rollback rapide et interrupteurs d’arrêt. Versionnez votre jeu de dépendances comme vous versionnez le code. En cas d’incident, vous devez disposer d’un interrupteur unique pour geler les mises à jour de dépendances, revenir au dernier état sain, et invalider les caches compromis en quelques minutes.

Un déploiement concret : 0–7, 30 et 90 jours

Semaine 1 : stopper l’hémorragie

  1. Gelez la dérive. Imposez npm ci ou l’équivalent pnpm/yarn en mode lockfile figé dans la CI immédiatement. Rejetez les PR qui mettent à jour les lockfiles sans revue.
  2. Coupez les scripts d’installation en CI. Ajoutez --ignore-scripts partout. Créez une petite liste d’autorisation pour les rares packages qui ont réellement besoin de scripts d’installation. Si votre build casse, c’est un signal pour remplacer le package ou le vendoriser.
  3. Figez le gestionnaire de paquets. Activez Corepack et figez les versions de npm/pnpm/yarn dans le dépôt. Sans cela, différents devs et runners se comportent différemment.
  4. Générez des SBOM dès maintenant. Intégrez la génération CycloneDX dans les builds et stockez les artifacts avec leurs SBOM. Cela prend une journée et paie à chaque nouveau gros titre.
  5. Ajoutez des règles de sortie pour la CI. Restreignez le trafic sortant à votre store d’artifacts et votre registre. Bloquez registry.npmjs.org directement si vous avez déjà un proxy privé ; sinon, mettez le proxy sur votre plan à deux semaines ci-dessous.

Jour 30 : construire le portail d’entrée

  1. Montez un registre privé. Verdaccio est la voie la plus rapide ; Artifactory/Nexus/CodeArtifact si vous avez besoin de workflows entreprise. Ne mirrorez que les packages approuvés. Activez la quarantaine en amont pour les nouveaux noms jusqu’à revue.
  2. Adoptez une analyse multi‑moteurs. Branchez Snyk ou OSV pour les CVE, Socket ou Phylum pour le comportement, et OpenSSF Scorecards pour l’hygiène. Échouez le build lorsqu’un package voit son profil de risque bondir entre versions (par ex. nouveau script d’installation ou appels réseau ajoutés).
  3. Placez tous les packages internes sous @yourorg. Mettez à jour les configs npm pour router le scope vers votre registre uniquement. Auditez les dépôts à la recherche de noms internes non scopés et corrigez-les.
  4. Commencez à enregistrer la provenance pour vos propres packages. Si vous publiez des packages internes dans votre registre privé, signez-les ou joignez la provenance de build. Si vous publiez en open-source, activez la provenance npm via GitHub Actions et communiquez‑la aux downstreams.
  5. Containerisez et rendez les builds reproductibles. Image de base Node figée, gestionnaire de paquets figé, lockfile figé, réseau à zéro excepté le registre. Enregistrez les digests des images dans vos métadonnées de build.

Jour 90 : réduire le rayon d’explosion

  1. Activez le modèle de permissions de Node pour les services. Commencez par tout refuser puis ouvrez le minimum de permissions fs et net par service. Oui, vous découvrirez des hypothèses trop permissives ; corrigez-les.
  2. Renforcez les runners CI. Conteneurs non privilégiés, pas de socket docker dans les étapes de build, profils seccomp, et isolement du système de fichiers pour les workspaces. Traitez vos runners comme des assets de production, pas des jouets jetables.
  3. Listes d’autorisation de sortie par service à l’exécution. Définissez les domaines et ports autorisés dans la configuration. Faites respecter via votre service mesh, un pare-feu eBPF ou des politiques réseau cloud.
  4. Formalisez le rollback. Versionnez vos ensembles de dépendances, mettez-les en cache dans le registre, et documentez une procédure d’urgence : geler les miroirs, bloquer un package, revenir aux lockfiles connus sains, et invalider les artifacts suspects.
  5. Entraînez l’exercice. Faites un table‑top basé sur un incident réel récent. Chronométrez les étapes : détection, évaluation d’impact (merci la SBOM), quarantaine, rollback et postmortem.

Décisions que vous devez réellement prendre

npm vs pnpm vs Yarn

Choisissez‑en un et standardisez‑le dans l’entreprise avec Corepack. Le store adressé par contenu et la rigueur de pnpm sont attrayants pour les monorepos, mais npm ci avec lockfiles figés est universellement compris. Le mauvais choix est d’autoriser la coexistence de trois outils.

Registre privé : construire ou acheter

  • Verdaccio : déploiement rapide, excellent cache, contrôle simple des scopes. Parfait des phases seed à Série B. Vous devrez ajouter des scanners et des politiques autour.
  • Artifactory/Nexus/CodeArtifact : moteurs de politique plus profonds, meilleure auditabilité et authentification entreprise. Plus à opérer, mais le bon choix si vous avez des dizaines d’équipes et des centaines de services.

Ce qu’il faut bloquer par défaut

  • Scripts de cycle de vie en CI. Autorisez des exceptions via liste d’autorisation.
  • Code obfusqué dans les packages. Blocage strict sauf revue et vendorisation.
  • Nouveaux mainteneurs/éditeurs sur des packages critiques. Quarantaine jusqu’à revue.
  • Packages avec compilateurs postinstall natifs (node-gyp) sur des chemins critiques. Préférez des prebuilts ou des alternatives.

Ce qu’il faut mesurer

  • Taux de déterminisme d’installation. Pourcentage de builds où les versions de dépendances correspondent exactement au dernier état sain. Cible : > 99,5 %.
  • Temps pour mesurer l’impact. Du gros titre à « quels services importent la version X ? ». Cible : minutes, via recherche SBOM.
  • Delta de dérive par semaine. Combien de packages ont été mis à jour sans ticket explicite et revue ? Cible : proche de zéro.
  • Événements de sortie bloqués. Vos règles d’egress en exécution et en CI arrêtent‑elles réellement les hôtes inconnus ? Vous voulez un bruit régulier et explicable, pas le silence.

Ce que l’incident TanStack change réellement

Il invalide l’heuristique « nous faisons confiance aux projets phares ». Les mainteneurs populaires sont précisément des cibles à très forte valeur. Il prouve aussi que la vitesse de propagation dépasse la vitesse de communication. Au moment où un mainteneur avertit sur Twitter ou publie un postmortem, des versions malveillantes peuvent déjà être dans vos caches et sur vos laptops.

Et avec des rapports crédibles sur l’accélération des découvertes et manipulations d’exploits par l’IA, vous devez vous attendre à des attaques plus fréquentes et plus convaincantes. Cela ne veut pas dire paniquer ; cela veut dire que votre état par défaut est « quarantaine d’abord, explications ensuite ». Vos outils décident de ce qui entre. Les humains expliquent pourquoi.

Comment opérer cela avec des équipes nearshore

Si vous dirigez des équipes distribuées entre les US et l’Amérique latine, vous avez besoin des mêmes garde‑fous partout. Les pratiques ci‑dessus ne ralentissent pas les équipes nearshore ; elles les accélèrent en supprimant les drames liés aux dépendances. Voici comment nous mettons cela en place dans des équipes mixtes US–Brazil :

  • Un seul registre, global. Tout le monde pointe vers le même miroir curaté. 6 à 8 heures de recouvrement de fuseaux horaires rendent l’astreinte pour les opérations du registre praticable.
  • Un unique gestionnaire de paquets et une version figée dans le dépôt. Fin du « ça marche avec mon npm ».
  • Stacks de départ pré‑approuvées. Templates avec dépendances verrouillées, scanners, SBOM et règles d’egress intégrés. Les nouveaux services démarrent sécurisés par défaut.
  • Réflexes d’incident. Exécutez le même exercice table‑top sur tous les sites. Rendez la recherche SBOM et les commandes de quarantaine du registre des automatismes.

Playbook : si une autre compromission tombe demain

  1. Identifiez l’exposition en minutes. Cherchez dans l’index SBOM le nom du package et les versions. Signalez les services affectés.
  2. Quarantaine au niveau du registre. Bloquez les versions malveillantes, videz les caches et geler le mirroring amont.
  3. Verrouillez les mises à jour de dépendances. Actionnez l’interrupteur pour stopper la dérive des lockfiles à travers les dépôts pendant 24–48 heures.
  4. Rebuildez depuis le dernier état sain. Utilisez un gestionnaire figé et des lockfiles gelés pour reproduire l’artifact d’hier. Comparez les digests.
  5. Patch ou remplacement. Si une version saine existe avec provenance, promouvez‑la via le registre et montez de version délibérément.
  6. Postmortem et mise à jour de politique. Le package a‑t‑il gagné des scripts d’installation ? Les mainteneurs ont‑ils changé ? Ajoutez une règle pour que la prochaine tentative soit bloquée automatiquement.

Objections fréquentes, réponses

« Cela va nous ralentir. »

Oui, pendant environ deux sprints. Puis cela vous rend plus rapides. Les équipes que nous avons basculées vers des registres curatés et des lockfiles figés passent beaucoup moins de temps à éteindre des incendies de builds cassés et de régressions surprises. Échangez une mise en place ponctuelle contre une stabilité durable.

« Nous ne pouvons pas bloquer les scripts d’installation — il nous faut des outils affûtés. »

Très bien. Faites une liste d’autorisation. Mais si 90 % des packages n’ont pas besoin de scripts de cycle de vie, pourquoi laisser 100 % d’entre eux exécuter du shell arbitraire sur vos builders ? Que l’exception prouve qu’elle est exceptionnelle.

« L’open‑source bouge trop vite pour des listes d’autorisation. »

Alors définissez une voie rapide avec des heuristiques automatisées et une revue après fusion. Par exemple : auto‑approbation des mises à jour mineures lorsque Scorecards reste au vert, que le diff de comportement est vide, et que la provenance est intacte. Tout le reste attend des yeux humains.

Le coût de ne pas faire cela

Supposez qu’un postinstall malveillant aspire vos secrets de CI et les pousse sur un pastebin. Votre temps médian pour faire tourner les tokens à l’échelle de votre plateforme se compte en jours. Votre distraction d’ingénierie totale effacera un sprint ou plus. Si cela survient une fois par an, le temps « gagné » en évitant d’investir dans un registre et des politiques est une illusion.

Le but n’est pas de rendre le développement JavaScript joyless. C’est d’arrêter de traiter npm comme un CDN où la vitesse prime sur l’examen. Vous ne feriez jamais curl | bash en production. npm install, c’est curl | bash avec une meilleure UX. Agissez en conséquence.

Points clés

  • Mettez un registre privé et curaté devant npm et faites‑en l’unique sortie pour les dépendances.
  • Faites respecter des lockfiles immuables et des installations déterministes avec npm ci ou équivalent.
  • Désactivez par défaut les scripts de cycle de vie en CI ; n’autorisez que lorsque nécessaire.
  • Adoptez une analyse multi‑moteurs (CVE, comportement, hygiène) et générez des SBOM à chaque build.
  • Activez le modèle de permissions de Node et des listes d’autorisation de sortie à l’exécution pour réduire l’impact.
  • Standardisez un gestionnaire de paquets via Corepack et figez les versions à l’échelle de l’organisation.
  • Exercez le drill d’incident : recherche SBOM, quarantaine au registre, rollback et communication.
  • Attendez‑vous à des attaques accélérées par l’IA ; concevez pour « quarantaine d’abord, explications ensuite ».

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation