Attaques npm : comment protéger votre serveur Node
Découvrez comment une release npm malveillante, un script postinstall ou un typosquat atteint votre VPS, et quelle pratique de déploiement bloque ces attaques.
Qu’est-ce qu’une attaque de la supply chain npm sur votre serveur
Une attaque de la supply chain npm atteint votre serveur par l’intermédiaire d’un package que vous avez choisi d’installer. Aucun port ouvert n’est impliqué et aucune étape d’exploitation n’est nécessaire. npm (node package manager) installe du code, et l’installation de code exécute du code. Une petite application Node récupère donc plusieurs centaines de packages que vous n’avez jamais examinés, et n’importe lequel peut publier une nouvelle version dans une heure.
Votre déploiement récupère une version malveillante parce que votre commande d’installation demande la dernière version compatible. Ce code s’exécute ensuite avec les privilèges de l’utilisateur qui a lancé l’installation. Tout ce qui suit découle de ces deux phrases.
Les différents cas sont classés selon la fréquence à laquelle ils touchent une personne qui déploie une application Node sur un seul VPS. Une grande entreprise ne les classerait pas dans cet ordre, car elle dispose d’un registre interne, d’une équipe de revue et d’un miroir du registre public. Vous avez un script de déploiement.
Forme 1 : le compte d’un mainteneur est compromis et publie un correctif
Le registre npm ne permet à personne de modifier le contenu d’une version qui existe déjà. Un attaquant qui hameçonne un mainteneur ou vole un jeton de publication ne peut donc pas réécrire 4.18.2. Il publie 4.18.3.
Consultez votre package.json. Une ligne comme "express": "^4.18.2" ne signifie pas la version 4.18.2. Le caret signifie « toute version 4.x supérieure ou égale à celle-ci », et ~4.18.2 signifie « toute version 4.18.x ». npm install résout cette plage au moment de son exécution. Le même commit Git, déployé deux fois le même après-midi, peut donc installer deux ensembles de code différents. Cet écart constitue la surface d’attaque. Pour qu’elle s’ouvre, rien n’a besoin d’être compromis sur votre machine.
Les versions malveillantes sont généralement signalées puis retirées, mais ce retrait intervient après leur installation. Toute personne ayant effectué un déploiement pendant cette période a le code sur son disque. Un pipeline qui résout les plages à chaque exécution entre automatiquement dans cette période plusieurs fois par semaine, sans que personne ne l’ait décidé.
Forme 2 : un script d’installation s’exécute avec les droits de l’utilisateur qui effectue le déploiement
Le package.json d’un paquet peut déclarer preinstall, install, postinstall et prepare dans son bloc scripts. npm les exécute pendant l’installation. Ils ne sont pas isolés et personne ne les vérifie. Ce sont des commandes shell qui s’exécutent avec les droits de l’utilisateur ayant saisi la commande d’installation, dans le répertoire personnel de cet utilisateur, avec son accès réseau et l’environnement complet de ce shell.
La bonne question n’est donc pas ce que le paquet peut faire. C’est ce que cet utilisateur peut lire. Sur un serveur de déploiement standard, cela inclut ~/.npmrc, qui contient un token de registre, ~/.ssh/id_ed25519, utilisé comme clé de déploiement pour SSH (secure shell), ~/.aws/credentials, ~/.docker/config.json, ainsi que toutes les variables exportées dans le shell. C’est généralement là que se trouve DATABASE_URL.
Une charge utile de ce type n’a besoin ni de persistance ni d’une élévation de privilèges. Elle lit quelques fichiers, les envoie à un hôte via HTTPS, puis se termine avec le code d’état 0. Vous ne voyez rien, car npm masque par défaut la sortie des scripts d’installation. Désactivez ce comportement et surveillez ce qui s’exécute réellement :
npm ci --foreground-scriptsforeground-scripts partage l’entrée, la sortie et l’erreur standard avec le processus npm. Les scripts de build affichent donc leur sortie dans votre terminal au lieu de l’écrire dans un buffer que npm supprime lorsque l’installation réussit.
Forme 3 : les typosquats et le nom que vous n’avez pas tout à fait saisi
Un typosquat est un paquet publié sous un nom proche d’un nom connu, dans l’attente d’une commande d’installation mal saisie ou mal collée. Le mécanisme repose sur la commande, pas sur le code. Un lockfile ne vous aide donc pas ici : vous ajoutez le mauvais nom une seule fois, puis le lockfile le verrouille fidèlement.
La variante qui vise les équipes plutôt que les individus est la confusion de dépendances. Votre paquet interne s’appelle billing-utils et se trouve sur un registre privé. Si aucun paquet nommé billing-utils n’existe sur le registre public, n’importe qui peut en publier un. npm résout les noms sans portée sur le registre public par défaut. La copie publique peut donc être sélectionnée. La solution consiste à utiliser une portée que vous contrôlez et à configurer un mappage de registre pour cette portée, dans .npmrc :
@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}Désormais, @yourorg/billing-utils est toujours récupéré depuis cet hôte, car le mappage portée-registre est consulté avant le registre par défaut. Un nom interne sans portée ne dispose d’aucun mappage et n’est donc pas protégé.
Avant d’ajouter une nouvelle dépendance, examinez-la plutôt que son indicateur de téléchargements :
npm view some-lib repository.url maintainers time.created time.modifiedUn paquet créé le mois dernier et publié par un compte que vous ne pouvez pas relier à un dépôt public présente un risque différent de celui d’un paquet ayant six ans d’historique. Aucun de ces éléments ne constitue une preuve. Ils sont tous deux rapides à vérifier.
Forme 4 : la dépendance dont le propriétaire a changé discrètement
Les mainteneurs se transmettent les paquets. Une personne s’épuise, un inconnu propose son aide, les droits de publication changent de titulaire, et aucun avis ne parvient aux projets qui en dépendent. Rien n’est compromis. La confiance que vous accordiez en 2021 est désormais détenue par une autre personne.
C’est la forme la plus lente et la plus difficile à détecter, et aucune commande ne permet d’y répondre directement. Deux vérifications permettent de réduire le risque. Vérifiez qui peut publier avant d’adopter un paquet, avec la ligne npm view ci-dessus. Lisez ensuite le diff lorsqu’un paquet dont vous dépendez réellement évolue :
npm diff --diff-name-only --diff=some-lib@1.4.2 --diff=some-lib@1.4.3
npm diff --diff=some-lib@1.4.2 --diff=some-lib@1.4.3La première forme affiche uniquement les noms des fichiers modifiés. Elle est donc assez rapide à exécuter lors de chaque mise à niveau d’un paquet important pour vous. Une version corrective qui modifie un script de build, ajoute un fichier à la racine du paquet ou édite le bloc scripts mérite une lecture complète avant d’atteindre votre serveur.
Construire à partir d’un lockfile validé avec npm ci
package-lock.json enregistre la version exacte de chaque package de l’arbre, l’URL d’origine de chaque package, un hash d’intégrité sha512 pour chaque tarball et le package qui en dépend. Commitez-le. C’est le seul fichier qui indique ce que vous avez réellement testé.
Installez ensuite avec npm ci, jamais avec npm install, sur toute machine qui n’est pas le laptop d’un développeur :
npm ci --omit=dev --ignore-scriptsnpm ci diffère de npm install de plusieurs façons importantes ici. Il exige la présence d’un lockfile. Il supprime tout node_modules existant avant de commencer. Les fichiers restants d’un déploiement précédent ne peuvent donc pas être conservés dans celui-ci. Il n’écrit jamais dans package.json ni dans le lockfile. Une installation ne peut donc pas vous faire passer discrètement à une version plus récente. Si le lockfile et package.json ne correspondent pas, la commande se termine avec une erreur au lieu de résoudre la différence.
Cette erreur est une fonctionnalité, pas un problème. Elle signifie qu’une modification de dépendance doit arriver sous la forme d’un commit relu et validé, et non comme un effet secondaire d’un déploiement à 02:00.
Le hash d’intégrité est vérifié à chaque téléchargement. Si les octets d’un tarball ne correspondent pas au hash enregistré, l’installation échoue avec code EINTEGRITY au lieu de décompresser le fichier. Il faut être précis sur ce que cela garantit : le fichier reçu est bien celui référencé par le lockfile. C’est la même garantie que vérifier les téléchargements avec des checksums, avec les mêmes limites. Cela ne dit rien sur le caractère malveillant de la version référencée au moment de sa publication.
Un détail concernant --omit=dev : ces packages sont tout de même résolus et inscrits dans le lockfile. Ils ne sont simplement pas placés sur le disque. Moins de packages sur le disque signifie moins de scripts d’installation et moins de code chargé à l’exécution. Cette option est donc utile. Elle ne supprime pas une dépendance de votre arbre.
Traitez les scripts d’installation comme du code et sachez les refuser
Vous pouvez désactiver les scripts d’installation. Ajoutez ceci au fichier .npmrc du projet et validez-le avec le lockfile :
ignore-scripts=true
save-exact=trueignore-scripts=true empêche npm d’exécuter les scripts déclarés dans les dépendances. save-exact=true demande à npm install some-lib d’écrire 1.4.2 dans package.json au lieu de ^1.4.2. Ainsi, une plage de versions ne se retrouve jamais par erreur dans votre manifest.
Cette option provoque des erreurs. Vous devez savoir lesquelles avant de l’activer. Les paquets qui compilent un module natif ou téléchargent un binaire précompilé effectuent ce travail dans un script d’installation. Lorsque les scripts sont désactivés, l’installation réussit, mais l’échec apparaît plus tard, à l’exécution. Le module ne peut alors pas charger son fichier de liaison. La solution consiste à définir une liste d’autorisation :
npm ci --ignore-scripts
npm rebuild better-sqlite3npm rebuild <package> exécute les scripts de build de ce paquet uniquement. Vous prenez ainsi une décision pour chaque paquet, au lieu d’accorder une autorisation générale d’exécution à plusieurs centaines de paquets dont vous ne connaissez pas les auteurs.
Pour connaître l’étendue actuelle de cette autorisation, interrogez npm :
npm query ":attr(scripts, [postinstall])"Cette commande affiche tous les paquets de l’arborescence installée qui contiennent un script postinstall. Pour une application classique, la liste est plus courte que prévu. C’est précisément ce qui rend la liste d’autorisation pratique.
Séparer la construction du processus qui traite le trafic
L’utilisateur de déploiement doit pouvoir écrire dans node_modules. Le processus qui répond aux requêtes HTTP n’en a pas besoin. S’il s’agit du même compte, le code exécuté pendant l’installation peut réécrire le code qui sert vos utilisateurs. Le code exécuté à l’exécution peut également le réécrire.
Séparez ces rôles. Construisez avec un utilisateur, servez avec un autre et rendez le répertoire servi accessible en lecture seule au compte de service :
sudo useradd --system --home-dir /srv/nodeapp --shell /usr/sbin/nologin nodeapp
sudo chown -R deploy:nodeapp /srv/nodeapp
sudo chmod -R o-rwx /srv/nodeappLaissez ensuite systemd appliquer cette séparation. Écrivez /etc/systemd/system/nodeapp.service :
[Unit]
Description=Node application
After=network-online.target
[Service]
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp/current
EnvironmentFile=/etc/nodeapp/env
ExecStart=/usr/bin/node server.js
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/srv/nodeapp/shared
NoExecPaths=/srv/nodeapp/shared
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.targetProtectSystem=strict monte l’ensemble du système de fichiers en lecture seule pour ce service, à l’exception de /dev, /proc, /sys et des chemins indiqués dans ReadWritePaths. Toute tentative de l’application d’écrire dans node_modules échoue donc avec EROFS: read-only file system. Vous pouvez le vérifier et retrouver cette erreur dans vos propres journaux en environ une minute. NoExecPaths concerne le répertoire d’envoi accessible en écriture : le service peut y créer des fichiers, mais le kernel refuse de les exécuter. Cette option nécessite systemd 249 ou une version ultérieure. Ubuntu 24.04 fournit la version 255.
Cette unité contient deux pièges. Premièrement, n’ajoutez pas MemoryDenyWriteExecute=yes. Cette option figure dans la plupart des listes de durcissement systemd. Elle empêche Node de démarrer, car V8 compile le code JavaScript en code machine à l’exécution et a besoin de pages qui sont à la fois accessibles en écriture et exécutables. Deuxièmement, récupérez le chemin ExecStart depuis command -v node. Si Node a été installé avec un gestionnaire de versions, il se trouve dans le répertoire personnel de l’utilisateur de déploiement. ProtectHome=yes masque alors ce répertoire pour le service, et l’unité échoue immédiatement avec status=203/EXEC et une ligne de journal indiquant que l’exécutable est introuvable.
Vérifiez le résultat au lieu de faire confiance au fichier :
sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemd-analyze security nodeapp.service
sudo -u nodeapp touch /srv/nodeapp/current/probesystemd-analyze security liste chaque paramètre de durcissement avec son niveau d’exposition. Vous pouvez ainsi voir lesquels utilisent encore leur valeur par défaut. touch doit échouer avec Permission denied, car nodeapp ne possède rien sous current. S’il réussit, les propriétaires de vos fichiers sont incorrects et les paramètres systemd masquent discrètement ce problème.
Une précision concernant EnvironmentFile : systemd le lit en tant que root avant de passer à User=nodeapp. Ce fichier peut donc être root:root avec le mode 600. L’application reçoit toujours les variables. Toute personne disposant d’un shell avec le compte nodeapp peut néanmoins les lire depuis /proc/<pid>/environ. Cette protection sécurise donc le secret au repos, mais pas le processus en cours d’exécution.
Exclure les identifiants de déploiement de l’environnement de build
Les scripts d’installation héritent de l’environnement. Ce seul fait doit déterminer l’endroit où vous effectuez le build.
La solution la plus sûre consiste à effectuer le build ailleurs que sur le serveur de production, puis à y copier le répertoire final. La machine de build ne conserve alors qu’un token de registre en lecture seule. Elle ne contient aucune autre information sensible : ni clé de déploiement SSH, ni clé d’accès cloud, ni mot de passe de base de données, ni identifiant de registre de conteneurs.
npm token create --read-onlyUn token en lecture seule peut télécharger des paquets, mais pas en publier. S’il est dérobé dans l’environnement de build, la perte se limite à la possibilité de télécharger des paquets publics.
Si vous devez effectuer le build sur le serveur, exécutez-le avec l’utilisateur deploy et un environnement volontairement réduit. Conservez les secrets d’exécution dans /etc/nodeapp/env, que deploy ne peut pas lire. Le même raisonnement s’applique à l’automatisation du build que vous hébergez vous-même : un runner GitHub Actions auto-hébergé contient des tokens et exécute du code publié arbitraire à chaque job. C’est donc la machine qui a le plus de valeur dans un petit déploiement. Tout programme que vous n’avez pas écrit et qui reçoit l’intégralité de votre environnement appartient à la même catégorie. C’est pourquoi exclure les secrets de l’environnement d’un agent d’IA revient au même problème, avec un autre programme intercalé.
Épinglez ou intégrez le fournisseur lorsque vous ne pouvez pas l’auditer
Une dépendance épinglée est une dépendance dont la version ne peut pas changer sans commit. Le fichier de verrouillage commité garantit déjà ce comportement pour toute l’arborescence. Deux cas nécessitent toutefois une mesure supplémentaire.
Les dépendances transitives sont le premier cas. Vous ne contrôlez pas les dépendances de vos propres dépendances. overrides dans package.json force une version partout dans l’arborescence :
{
"overrides": {
"some-transitive-lib": "1.4.2"
}
}Exécutez npm install une fois après l’avoir ajouté afin d’enregistrer le résultat dans le fichier de verrouillage, puis commitez les deux fichiers.
Le deuxième cas concerne un paquet que vous ne pouvez ni auditer ni supprimer. Intégrez-le au dépôt. npm pack télécharge l’archive tar exacte que le registre fournirait, et une dépendance file: s’installe depuis votre copie :
npm pack some-lib@1.4.2
mkdir -p vendor && mv some-lib-1.4.2.tgz vendor/{
"dependencies": {
"some-lib": "file:vendor/some-lib-1.4.2.tgz"
}
}L’archive tar se trouve désormais dans votre dépôt et ne peut plus être modifiée à votre insu. Vous devez toutefois assurer ses mises à jour indéfiniment. Utilisez donc cette méthode pour le petit paquet abandonné dont vous ne pouvez pas vous passer, pas pour votre framework web.
Il existe également une période de refroidissement qui ne coûte rien :
npm install --before=2026-08-01L’option before reconstruit l’arborescence en utilisant uniquement les versions publiées à cette date ou avant. Lorsque vous actualisez les dépendances, définissez une date antérieure d’une ou deux semaines. Vous évitez ainsi la période pendant laquelle une version défectueuse est disponible mais n’a pas encore été signalée. C’est un outil rudimentaire, car il retarde également les véritables correctifs de sécurité. Utilisez-le pour résoudre les plages de versions, examinez les changements, puis commitez le fichier de verrouillage. Le même principe s’applique aux outils en ligne de commande que vous installez depuis npm au lieu de les déclarer comme dépendances : un appel npx sans version épinglée récupère ce qui a été publié ce matin, tandis que épingler une version exacte de dsh permet à deux machines d’exécuter le même code.
Comment savoir quelle version a réellement été déployée ?
Le lockfile dans git indique ce qui aurait dû être installé. Le disque indique ce qui est installé. Seul le second constitue une preuve.
npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"npm ls lit node_modules et indique donc ce qui est physiquement présent, plutôt que ce que le lockfile prévoyait. La ligne node -e lit le manifeste installé à partir de son chemin. Elle fonctionne même pour les packages dont le champ exports bloque les imports de sous-chemins, et affiche une seule version sans arborescence autour.
Pour l’autre moitié de la comparaison, consultez git :
git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'Rendez la correspondance permanente en intégrant le commit à la structure de déploiement. Effectuez la release dans /srv/nodeapp/releases/<short commit sha> et pointez /srv/nodeapp/current vers celle-ci avec un lien symbolique. La réponse à « quelle version s’exécute actuellement ? » devient readlink /srv/nodeapp/current. Elle est également disponible à 03:00 pour une personne qui n’est pas celle qui a effectué le déploiement.
Enfin, vérifiez ce que le registry peut attester :
npm audit signaturesCette commande vérifie les signatures du registry pour les packages présents dans votre arborescence installée. Elle vérifie aussi les attestations de provenance pour les packages qui en disposent. La provenance relie un tarball publié au build d’intégration continue (CI) public qui l’a produit. Une attestation vérifiée permet donc de remonter le code jusqu’à un commit, plutôt que jusqu’à un laptop inconnu. La couverture n’est pas universelle. Une attestation manquante signifie donc « aucune information », et non « package défectueux ».
Que faire après la mise en production d’une version malveillante
Commencez par déterminer ce qui a été exécuté et avec quel utilisateur.
Si le code a été exécuté pendant l’installation, considérez que tout ce qui était lisible par l’utilisateur de build est compromis. Faites tourner le token du registre, les clés SSH présentes dans son répertoire personnel, les identifiants cloud et tous les secrets exportés dans ce shell. La rotation est la seule réponse rigoureuse, car vous ne pouvez pas prouver qu’un fichier n’a pas été lu.
Si le code a été exécuté au runtime sous un compte de service aux privilèges limités, l’ensemble accessible est beaucoup plus réduit : les variables d’environnement de l’application et tout ce que ses accès réseau peuvent atteindre. C’est tout l’intérêt de faire fonctionner les services avec des utilisateurs non privilégiés sur un VPS. Cela n’empêche pas la compromission. Cela détermine la partie de la machine à laquelle elle donne accès et sa capacité à survivre à un redémarrage.
Reconstruisez ensuite au lieu de nettoyer l’installation existante. Supprimez node_modules, bloquez le paquet concerné à une version antérieure à la version malveillante dans package.json, exécutez npm install une fois pour mettre à jour le lockfile, validez la modification, puis déployez avec npm ci. Ne réparez pas une arborescence en place. Vous ne pouvez pas recenser tout ce qu’un script d’installation a modifié.
Notez également la fenêtre concernée : le premier déploiement susceptible d’avoir récupéré la version et le déploiement qui l’a supprimée. Cette période vous indique quels journaux consulter. Vous ne pouvez la déterminer que si vos releases portent le nom des commits.
Ce que tout cela ne résout pas
Un fichier de verrouillage ne rend pas une dépendance sûre. Il transforme le moment où vous avez accepté cette dépendance en une décision datée et réexaminée, au lieu d’en faire un effet secondaire d’un déploiement. Toutes les pratiques ci-dessus effectuent la même conversion : elles transforment un accident en choix.
npm audit n’est pas une défense dans ce cas. Cet outil compare votre arborescence à une base de données de vulnérabilités signalées. Il détecte donc les problèmes qui ont déjà été publiés et identifiés. Une attaque de la chaîne d’approvisionnement reste sans nom pendant toute sa période utile. Exécutez npm audit pour rechercher les anciennes vulnérabilités connues, mais n’en attendez rien concernant une version publiée il y a quatre heures.
Réduire le nombre de vos dépendances aide davantage que n’importe quel outil présenté dans ce guide. C’est aussi le conseil le moins populaire. Chaque package que vous n’ajoutez pas représente un éditeur de moins susceptible d’être victime d’un hameçonnage en votre nom, ainsi qu’un script d’installation de moins exécuté avec les droits de votre utilisateur de déploiement.
Rien de tout cela n’est propre à npm. Les mêmes quatre formes s’appliquent à PyPI, RubyGems, aux images de conteneurs et au gestionnaire de paquets de votre distribution. Le problème apparaît surtout avec npm, car les arbres de dépendances sont plus profonds et les scripts d’installation s’exécutent par défaut. Tout ce qui étend un outil que vous utilisez déjà hérite du même problème. C’est pourquoi déterminer ce à quoi un plugin dsh peut accéder avant de l’installer revient au même que lire un script postinstall, en remplaçant les permissions de votre utilisateur de déploiement par celles de votre agent. La partie de la machine environnante que vous devez protéger dépend de l’endroit où l’outil s’exécute. Cela relève de la question plus générale de la sécurité de l’hébergement VPS.
FAQ
npm ci me protège-t-il contre un paquet npm compromis ?
Il vous protège contre une modification de version à votre insu. npm ci installe exactement ce que package-lock.json enregistre, vérifie chaque tarball avec son hash d’intégrité sha512 et s’arrête avec une erreur si package.json et le lockfile ne correspondent pas, au lieu de résoudre la différence. Cela ne dit rien sur la sécurité de la version verrouillée. Si vous validez un lockfile qui verrouille une version malveillante, npm ci installera fidèlement cette version sur chacun de vos serveurs, à chaque fois.
Dois-je définir ignore-scripts=true pour tout ?
Définissez-le, puis ajoutez des exceptions à une allowlist. ignore-scripts=true dans le .npmrc du projet empêche l’exécution des scripts d’installation des dépendances, ce qui supprime le chemin le plus direct entre un paquet malveillant et les identifiants de l’utilisateur de déploiement. Les paquets qui compilent un module natif ou téléchargent un binaire précompilé ont réellement besoin de leurs scripts. Avec les scripts désactivés, ils échouent plus tard à l’exécution avec un fichier de binding manquant, au lieu d’échouer lors de l’installation. Exécutez npm ci --ignore-scripts, puis npm rebuild <package> pour les quelques paquets auxquels vous avez décidé de faire confiance. npm query ":attr(scripts, [postinstall])" indique combien il y en a réellement.
Comment savoir quelle version d’un paquet mon serveur a réellement installée ?
Lisez le contenu du disque, pas le lockfile. npm ls <package> indique ce qui est présent dans node_modules, et node -e "console.log(require('./node_modules/<package>/package.json').version)" affiche uniquement la chaîne de version. Le lockfile dans git répond à une autre question : ce qui aurait dû être installé. La comparaison des deux est précisément le but. Déployer dans un répertoire nommé d’après le commit git permet de conserver ces deux informations, même plusieurs mois plus tard, lorsque vous en avez besoin.
npm audit détecte-t-il les attaques de la supply chain ?
Non. npm audit compare votre arbre de dépendances à une base de données de vulnérabilités signalées. Il ne détecte donc que les problèmes déjà publiés et associés à un identifiant. Une release malveillante peut rester non signalée pendant les heures ou les jours où son installation est importante. npm audit signatures est la commande la plus utile : elle vérifie les signatures du registry pour l’ensemble des paquets installés et contrôle les attestations de provenance lorsqu’elles ont été produites par l’éditeur. Elle indique ainsi qu’un tarball provient d’un build public plutôt que d’une machine inconnue.
Pourquoi est-il important d’exécuter l’application avec un utilisateur non privilégié si l’attaque se produit lors de l’installation ?
Parce que ces deux scénarios ont une portée différente et que vous devez vous protéger contre les deux. Le code exécuté lors de l’installation s’exécute avec les droits de l’utilisateur de déploiement. Il peut lire les clés SSH, les tokens du registry et les identifiants cloud de cet utilisateur. Le code exécuté à runtime s’exécute avec le compte de service. Avec User=nodeapp, ProtectSystem=strict et aucun identifiant sur le disque, sa portée se limite à l’environnement de l’application et à sa base de données. La séparation des comptes empêche également le processus qui sert le trafic de réécrire node_modules. Une compromission à runtime disparaît donc au redémarrage suivant au lieu de devenir permanente.