Aller au contenu principal
brutweb.fr fr / dev web
CODE_BRUT

Le développement web, sans vernis. — par Julien Mercier

Sécurité

Sécuriser npm en 2026 sans bloquer les livraisons

Attaques sur npm, paquets compromis et CI : les protections qui réduisent vraiment le risque sans transformer chaque déploiement en parcours d’obstacles.

Par Julien Mercier 7 min de lecture
Sécuriser npm en 2026 sans bloquer les livraisons

La chaîne npm est désormais une cible de production

Une application web moderne dépend rarement de quelques paquets isolés. Entre le framework, les outils de compilation, les bibliothèques d’interface, les clients API, les outils de test et les plugins de linting, un simple package.json ouvre la porte à un arbre de dépendances bien plus large que ce que l’équipe a choisi consciemment.

Ce constat ne signifie pas que npm est intrinsèquement dangereux. Il signifie qu’une dépendance est du code tiers exécuté dans un contexte qui peut être sensible : le poste d’un développeur, le pipeline CI, un serveur de build ou, dans le pire cas, l’environnement de production. Le risque n’est pas seulement théorique.

L’écosystème JavaScript a déjà connu plusieurs incidents instructifs :

  • En 2018, le paquet event-stream a intégré une dépendance malveillante visant un portefeuille de cryptomonnaie précis.
  • La même année, eslint-scope a été compromis après le vol d’identifiants d’un mainteneur.
  • En 2021, des versions de ua-parser-js ont été publiées avec du code malveillant après la compromission du compte du mainteneur.
  • En 2022, les paquets colors et faker ont rappelé qu’un changement destructeur peut aussi venir d’un mainteneur légitime, sans attaque externe.

Ces cas recouvrent des scénarios différents : compte npm compromis, transfert de maintenance insuffisamment vérifié, paquet abandonné, dépendance indirecte opaque ou publication volontairement nuisible. Les traiter tous avec un unique npm audit revient à confondre détection de vulnérabilités connues et sécurité de la chaîne d’approvisionnement.

Le sujet devient critique parce que la CI possède souvent plus de droits qu’elle ne devrait. Un workflow GitHub Actions, GitLab CI ou autre peut disposer de secrets de déploiement, de clés d’API, d’un accès au registre, voire de permissions d’écriture sur le dépôt. Si l’installation d’un paquet compromis s’exécute dans ce contexte, les conséquences dépassent largement le navigateur d’un utilisateur.

La bonne approche n’est donc pas de bloquer toute mise à jour ou de soumettre chaque livraison à dix validations manuelles. Elle consiste à réduire les chemins d’attaque les plus plausibles, là où ils ont réellement de l’impact.

Un lockfile est indispensable, mais il ne constitue pas une preuve de confiance

Pour une application npm, versionner un lockfile est une base non négociable. Qu’il s’agisse de package-lock.json, npm-shrinkwrap.json, yarn.lock ou pnpm-lock.yaml, son rôle principal est de figer la résolution des dépendances.

Avec npm ci, la CI installe l’arbre décrit dans package-lock.json plutôt que de recalculer les versions compatibles avec les plages indiquées dans package.json. C’est un gain majeur de reproductibilité : un build de demain a bien plus de chances d’utiliser les mêmes versions qu’un build d’aujourd’hui.

Le lockfile stocke également des informations d’intégrité pour les archives téléchargées. Cela aide à détecter une archive modifiée entre sa publication et son installation. Mais il faut être clair sur ses limites :

  • Il ne garantit pas qu’un paquet était sain au moment où sa version a été verrouillée.
  • Il ne protège pas contre du code malveillant publié légitimement avec un compte compromis ou mal géré.
  • Il ne juge pas le comportement d’un script postinstall, d’un binaire téléchargé ou d’une dépendance native.
  • Il ne corrige pas une dépendance ancienne dont les vulnérabilités sont découvertes après son adoption.

La règle opérationnelle est simple : utilisez npm ci dans la CI et faites échouer le build si le lockfile n’est pas cohérent avec package.json. Évitez npm install pour une installation de production ou de CI, car cette commande peut modifier le lockfile et faire évoluer la résolution selon le contexte.

Le lockfile doit aussi être lu comme un artefact de livraison. Une pull request qui modifie des dizaines de versions indirectes sans justification mérite une revue plus attentive qu’un changement applicatif banal. Les outils comme Dependabot et Renovate rendent ces évolutions visibles et découpables ; ils ne remplacent pas la revue, mais évitent les mises à jour massives, rares et difficiles à comprendre.

Pourquoi npm audit et les mises à jour automatiques ne suffisent pas

npm audit reste utile. Il compare l’arbre installé aux avis de sécurité connus par le registre npm et remonte des vulnérabilités publiées. C’est un contrôle peu coûteux qui doit figurer dans un pipeline ou, au minimum, dans une vérification régulière.

Mais un audit ne voit que ce qui est déjà identifié, documenté et associé à une version de paquet. Une dépendance malveillante inédite, un typosquatting, une fuite de secret dans un script d’installation ou un compte mainteneur compromis peuvent passer au travers. De même, une alerte concernant une dépendance réservée au développement n’a pas automatiquement le même impact qu’une vulnérabilité chargée dans le bundle serveur exposé sur Internet.

Traiter chaque résultat avec la même sévérité produit souvent l’effet inverse de celui recherché : des développeurs habitués à voir un pipeline rouge pour des alertes indirectes et non exploitables finissent par les ignorer. Il vaut mieux définir des règles explicites :

  • bloquer immédiatement les vulnérabilités critiques ou élevées qui affectent un chemin de production identifiable ;
  • évaluer les alertes transitives selon l’usage réel du paquet ;
  • créer un suivi daté pour les correctifs impossibles à appliquer immédiatement ;
  • ne jamais utiliser npm audit fix --force aveuglément dans une branche de livraison.

Le dernier point est important. L’option --force peut conduire npm à effectuer des changements majeurs de versions. Elle peut donc masquer une vulnérabilité sous une régression fonctionnelle ou une incompatibilité de build. Un correctif de sécurité qui casse le paiement, l’authentification ou le rendu serveur n’est pas un processus de sécurité mature.

Les mises à jour automatiques ont néanmoins une vraie place. Configurez Dependabot ou Renovate pour ouvrir des pull requests ciblées, idéalement regroupées par écosystème ou par niveau de risque. Une mise à jour régulière de petites tailles réduit l’écart avec les versions maintenues. C’est beaucoup plus tenable qu’une « grande journée des dépendances » tous les six mois.

Cette logique rejoint celle d’une refonte technique progressive : comme expliqué dans notre article sur la refonte d’un site legacy sans tout casser, réduire les écarts en continu coûte généralement moins cher qu’un rattrapage brutal.

Réduire la surface d’attaque avant d’ajouter des contrôles

La protection la plus simple consiste souvent à installer moins de code. Avant d’ajouter une dépendance, posez trois questions : résout-elle un problème que le navigateur, Node.js ou votre framework ne résout pas déjà ? Est-elle maintenue ? Son coût opérationnel est-il proportionné au service rendu ?

Un paquet très petit n’est pas automatiquement plus sûr, et un paquet populaire n’est pas automatiquement fiable. En revanche, chaque paquet direct ajoute un fournisseur à évaluer, des dépendances transitives et un rythme de mises à jour à suivre.

Quelques pratiques concrètes réduisent la surface sans ralentir une équipe :

  • préférer les API natives lorsque le besoin est simple, notamment pour fetch, URL, AbortController ou les fonctions de date limitées ;
  • éviter les bibliothèques installées uniquement pour une fonction triviale ;
  • supprimer les dépendances inutilisées avec une revue régulière du manifeste ;
  • éviter les versions flottantes telles que *, latest ou des plages trop larges pour les dépendances critiques ;
  • utiliser des scopes npm pour les paquets internes, par exemple @votre-organisation/ui, afin de limiter les ambiguïtés de nommage.

La réduction des polyfills devenus inutiles relève de la même discipline. Notre analyse de Baseline et de l’abandon des polyfills superflus montre qu’une cible navigateur explicite peut aussi simplifier la chaîne de build. Moins de transformations, moins de plugins et moins de dépendances indirectes signifient moins de choses à surveiller.

Enfin, méfiez-vous des ajouts dictés par une urgence. Lorsqu’un développeur cherche un paquet pour convertir, parser, chiffrer ou scraper « en cinq minutes », le risque de sélectionner un nom trompeur ou peu maintenu augmente. Une petite liste interne de bibliothèques validées pour les besoins courants apporte plus de valeur qu’un catalogue de règles théoriques.

La provenance apporte un signal utile, pas une garantie absolue

La provenance répond à une question précise : peut-on relier un paquet publié à un processus de build identifiable, généralement un dépôt source et une exécution CI ? npm propose des mécanismes de provenance des paquets pour les publications réalisées depuis certains workflows compatibles, notamment GitHub Actions.

Pour un consommateur, ce signal est utile lors de l’évaluation d’une dépendance importante. Il aide à vérifier que la publication est liée à un dépôt attendu et à une chaîne d’automatisation plutôt qu’à une machine personnelle dont l’état est inconnu. Pour un mainteneur de paquet, publier avec provenance améliore aussi la traçabilité en cas d’incident.

Il ne faut toutefois pas surinterpréter ce mécanisme. Une provenance cohérente ne démontre pas que le code est sans vulnérabilité, que le dépôt n’a pas été compromis ou que les reviewers ont détecté une modification malveillante. Elle réduit une zone d’ombre : l’origine de l’artefact publié.

Pour les paquets internes ou les bibliothèques qui jouent un rôle critique, adoptez une règle simple : publication depuis une CI déclarée, jamais depuis le poste local d’un développeur. Les mécanismes de trusted publishing de npm s’appuient sur une identité fédérée du fournisseur CI. L’intérêt est concret : éviter de stocker un jeton npm de publication longue durée dans les secrets CI lorsque le workflow peut obtenir une identité temporaire et vérifiable.

Activez aussi l’authentification à deux facteurs pour les comptes ayant le droit de publier ou d’administrer des paquets. Ce n’est pas un contrôle sophistiqué, mais il répond directement à l’un des scénarios historiques les plus coûteux : le vol d’identifiants de mainteneur.

Isoler la CI : le point où les dégâts peuvent devenir majeurs

Une dépendance compromise est problématique. Une dépendance compromise exécutée dans une CI qui détient tous les secrets de production l’est beaucoup plus. La séparation des responsabilités dans le pipeline est donc un garde-fou à très fort rendement.

Commencez par séparer les étapes qui n’ont pas les mêmes besoins :

  • installation, lint, tests et build sur des environnements sans secrets de production ;
  • déploiement dans un job distinct, après validation du build ;
  • publication npm dans un workflow dédié, déclenché depuis une branche ou un tag protégé ;
  • tâches administratives exceptionnelles hors des workflows exécutés sur des pull requests non fiables.

Les scripts de cycle de vie npm méritent une attention particulière. Des scripts preinstall, install, postinstall ou prepare peuvent s’exécuter pendant l’installation. Dans certains jobs d’analyse, l’option npm ci --ignore-scripts permet de réduire cette exposition. Elle n’est pas universelle : certaines dépendances ont réellement besoin d’un script d’installation pour générer ou télécharger un binaire. Il faut donc l’appliquer sur les étapes où elle est compatible, puis conserver un job de build représentatif qui teste l’installation normale dans un environnement sans privilège.

Sur GitHub Actions, les permissions du GITHUB_TOKEN peuvent être configurées au niveau du workflow ou du job. Accordez le minimum nécessaire. Un job qui exécute les tests n’a généralement pas besoin d’écrire dans le dépôt, de créer une release ou d’accéder à un registre privé. La documentation GitHub recommande précisément de modifier les permissions du GITHUB_TOKEN selon le principe du moindre privilège.

Le même principe s’applique aux actions tierces. Référencez les actions sensibles par un identifiant immuable, idéalement un SHA de commit, plutôt que par une branche mobile ou un tag qui peut évoluer. Cette précaution ne remplace pas la confiance dans l’action choisie, mais elle évite qu’un workflow change silencieusement de comportement parce qu’une référence mutable a été déplacée.

Gérer les secrets et les droits sans créer une usine à gaz

La sécurité de npm ne se limite pas aux paquets téléchargés. Un jeton de publication ou un secret de déploiement mal exposé permet à un attaquant de publier une version frauduleuse, d’accéder à un registre privé ou de modifier une infrastructure.

Une politique réaliste tient en quelques règles compréhensibles :

  • ne placez jamais de jeton npm dans le dépôt, le lockfile, une image Docker ou les logs CI ;
  • préférez des identités temporaires fédérées, lorsqu’elles sont disponibles, aux secrets permanents ;
  • limitez les jetons restants à leur usage précis et retirez-les lorsqu’un collaborateur, un projet ou un workflow n’en a plus besoin ;
  • séparez les secrets de préproduction et de production ;
  • empêchez les pull requests externes d’accéder aux secrets de déploiement.

Un autre point mérite d’être traité explicitement : les forks et les contributions externes. Un workflow exécuté sur du code proposé par un tiers ne doit pas recevoir les mêmes droits qu’un workflow déclenché après fusion sur la branche principale. Sinon, une pull request peut transformer votre CI en mécanisme d’exfiltration de secrets.

Les protections de branche complètent ce dispositif. Exigez une revue pour les changements de workflow, de fichiers de dépendances et de configuration de publication. Ce ne sont pas des fichiers secondaires : ils définissent qui exécute quoi, avec quels droits et à quel moment.

Mettre en place une politique de dépendances tenable pour une équipe web

Une politique efficace doit être assez courte pour être suivie pendant une livraison urgente. Voici un socle raisonnable pour la plupart des équipes JavaScript :

  • un lockfile versionné et npm ci dans tous les builds reproductibles ;
  • des mises à jour automatiques en pull requests, revues et testées ;
  • un contrôle régulier des vulnérabilités connues avec tri selon l’exposition réelle ;
  • une validation renforcée pour les nouvelles dépendances directes et les paquets qui touchent à l’authentification, au chiffrement, au paiement ou à l’infrastructure ;
  • une CI sans secrets dans les jobs de test et de build ;
  • des permissions minimales pour les jetons et les workflows ;
  • une publication de paquets internes depuis une CI avec provenance et authentification renforcée.

Documentez aussi qui peut accepter une nouvelle dépendance directe, qui peut publier un paquet et qui est responsable du traitement des alertes. Il ne s’agit pas de créer un comité de validation pour chaque npm install. L’objectif est d’éviter que les décisions sensibles deviennent invisibles ou qu’elles reposent sur une seule personne.

Dans une petite équipe, une revue par un pair sur les nouvelles dépendances et les changements de CI suffit souvent. Dans une organisation plus large, une liste de paquets approuvés, un registre privé et des règles de dépôt peuvent compléter le dispositif. Le niveau de contrôle doit suivre l’impact de l’application, pas une checklist copiée d’un grand groupe.

Une chaîne npm sécurisée n’est pas celle qui ne change jamais. C’est celle qui peut changer régulièrement, de manière traçable, testée et avec des privilèges limités.

Conclusion : viser une sécurité qui accompagne les livraisons

Sécuriser npm en 2026 ne demande pas de transformer chaque déploiement en parcours administratif. Les mesures les plus rentables restent concrètes : verrouiller les versions, installer avec npm ci, mettre à jour souvent, réduire le nombre de dépendances, vérifier la provenance quand elle est disponible, isoler les jobs CI et retirer les privilèges inutiles.

Les audits et les scanners conservent leur utilité, mais ils ne compensent ni une CI sur-privilégiée ni un secret exposé à chaque installation. Commencez par examiner votre pipeline actuel : quels jobs exécutent du code tiers, quels secrets y sont accessibles et quels droits sont réellement nécessaires ? Cette revue ciblée fournit souvent les premiers gains de sécurité sans ralentir la prochaine livraison.