Aller au contenu

Sécuriser les dépendances npm, PyPI et Go

Mise à jour :

En 2024-2025, PyPI et npm ont retiré des milliers de packages malveillants. Le vecteur d’attaque ? Des noms de packages ressemblant à des packages légitimes (typosquatting) ou exploitant la confusion entre registres publics et privés (dependency confusion).

Ce guide vous donne les outils pour éviter d’installer du code malveillant dans vos projets.

Les vecteurs d’attaque

Typosquatting

Le typosquatting exploite les erreurs de frappe lors de l’installation. L’attaquant publie un package dont le nom ressemble à un package populaire, en espérant que des développeurs se trompent.

Exemples de pièges courants

Package légitimeTyposquatDifférence
lodashlodahsInversion de lettres
requestsrequestSingulier/pluriel
tensorflowtensorfowLettre manquante
@angular/coreangular-coreFormat différent

Comment l’attaque se produit

Vous voulez installer requests (le client HTTP Python le plus populaire) :

Terminal window
# Vous tapez trop vite...
pip install request # ❌ Typosquat ! Package malveillant
# Au lieu de :
pip install requests # ✅ Le vrai package

Le package malveillant request (sans “s”) contient un script qui s’exécute à l’installation. En quelques secondes, vos credentials AWS ou vos clés SSH sont exfiltrés vers un serveur de l’attaquant.

Le plus vicieux : l’installation semble fonctionner normalement. Le package malveillant inclut souvent le vrai code du package légitime pour ne pas éveiller les soupçons.

Dependency confusion

La dependency confusion exploite un comportement par défaut des gestionnaires de packages : quand un package existe sur plusieurs registres, c’est la version la plus récente qui gagne.

Comment ça marche ?

Imaginons une entreprise fictive “Acme Corp” :

  1. L’entreprise crée un package interne : @acme/auth-utils version 1.2.0, hébergé sur leur registre privé Artifactory.

  2. Un attaquant découvre ce nom (via une fuite de package.json, un message d’erreur, ou du code source public).

  3. L’attaquant publie sur npm public : @acme/auth-utils version 99.0.0 avec un script postinstall malveillant.

  4. Un développeur fait npm install : npm voit deux sources pour le même package. Sans configuration explicite, il choisit la version 99.0.0 (plus récente) du registre public.

  5. Le script malveillant s’exécute : vol de credentials, backdoor installée.

Pourquoi ça fonctionne ?

Comportement par défautConséquence
npm/pip cherchent d’abord sur le registre publicLe package malveillant est trouvé
La version la plus haute est préférée99.0.0 > 1.2.0
Les scripts s’exécutent automatiquementCode malveillant lancé à l’install

Comment se protéger ?

La solution est de forcer la résolution vers le registre privé pour vos packages internes. Voir les sections “Registre privé” plus bas pour npm, pip et Go.

Scripts d’installation malveillants

Les gestionnaires de packages permettent d’exécuter du code à l’installation :

GestionnaireScriptRisque
npmpostinstallExécution arbitraire
pipsetup.pyCode Python au install
GoPas de scripts✅ Plus sûr

Protection npm

npm est l’écosystème le plus ciblé par les attaquants (2+ millions de packages). Voici les protections à mettre en place, de la plus simple à la plus robuste.

Désactiver les scripts d’installation

Les scripts postinstall sont le vecteur d’attaque principal. Quand vous faites npm install, npm peut exécuter du code arbitraire défini par le mainteneur du package. C’est pratique pour compiler du code natif, mais c’est aussi une porte ouverte aux malwares.

Solution : désactiver les scripts par défaut.

Créez ou modifiez le fichier .npmrc à la racine de votre projet :

# .npmrc - Configuration npm du projet
ignore-scripts=true

Cette ligne indique à npm de ne jamais exécuter les scripts lors de l’installation. Si vous avez déjà un projet, vous pouvez aussi installer ponctuellement sans scripts :

Terminal window
npm install --ignore-scripts

Vérification : après installation, aucun message “Running postinstall” ne doit apparaître.

Vérifier avant installation avec npq

npq est un wrapper autour de npm install qui analyse le package avant de l’installer. Il détecte les signaux d’alerte que vous ne verriez pas autrement.

Terminal window
# Installer npq globalement (une seule fois)
npm install -g npq

Ensuite, utilisez npq au lieu de npm install :

Terminal window
# Au lieu de : npm install lodash
npq install lodash

Ce que npq vérifie automatiquement :

VérificationCe que ça détecte
TyposquattingNom similaire à un package populaire
MainteneursCompte compromis ou nouveau mainteneur suspect
ScriptsPrésence de postinstall ou scripts suspects
ÂgePackage créé récemment (< 7 jours = suspect)
PopularitéTéléchargements anormalement bas

Exemple de sortie suspecte :

⚠️ lodahs has mass typosquatting potential
⚠️ lodahs has a preinstall script
❌ Package installation blocked

Épingler les versions exactes

Par défaut, npm utilise des ranges de versions (^4.17.0 = “4.17.0 ou plus récent compatible”). Le problème : si le mainteneur publie une version malveillante 4.18.0, votre prochain npm install l’installera automatiquement.

Solution : épingler les versions exactes.

{
"dependencies": {
"lodash": "4.17.21",
"express": "4.18.2"
}
}
NotationSignificationRisque
"4.17.21"Exactement cette version✅ Sûr
"^4.17.0"4.17.0 à 4.x.x⚠️ Mises à jour auto
"~4.17.0"4.17.0 à 4.17.x⚠️ Mises à jour mineures
"*"N’importe quelle version❌ Dangereux

Configurer npm pour toujours épingler :

Terminal window
npm config set save-exact true

Lockfile obligatoire

Le fichier package-lock.json enregistre les versions exactes de toutes vos dépendances (directes et transitives) avec leurs checksums. C’est votre garantie de reproductibilité.

Règle absolue : toujours commiter le lockfile.

Terminal window
git add package-lock.json
git commit -m "chore: update lockfile"

En production, utilisez npm ci au lieu de npm install :

Terminal window
# ❌ npm install : peut modifier le lockfile
npm install
# ✅ npm ci : respecte strictement le lockfile
npm ci
CommandeComportementUsage
npm installPeut mettre à jour les dépendancesDéveloppement
npm ciInstalle exactement ce qui est dans le lockfileCI/CD, production

npm ci est aussi plus rapide car il supprime node_modules et réinstalle tout d’un coup au lieu de calculer les différences.

Registre privé (contre dependency confusion)

Si votre entreprise utilise des packages internes (ex: @acme/utils), vous devez forcer npm à les chercher uniquement sur votre registre privé.

Créez un fichier .npmrc à la racine du projet :

.npmrc
# Tous les packages @acme/* viennent UNIQUEMENT du registre privé
@acme:registry=https://npm.acme-corp.com/
# Le reste vient de npm public
registry=https://registry.npmjs.org/

Ce que ça change :

Sans cette configAvec cette config
npm cherche @acme/utils sur npm publicnpm cherche @acme/utils sur votre registre privé
Risque de dependency confusion✅ Impossible d’installer un faux package

Vérification : tentez d’installer un package @acme/* inexistant. Vous devez obtenir une erreur 404 de votre registre privé, pas de npm public.

Protection PyPI

PyPI (Python Package Index) est moins mature que npm en termes de sécurité. Il n’y a pas de vérification d’identité des mainteneurs, et les packages peuvent exécuter du code Python arbitraire à l’installation via setup.py.

Auditer les vulnérabilités avec pip-audit

pip-audit scanne vos dépendances installées et les compare à la base de données des vulnérabilités connues (OSV, PyPI Advisory Database).

Terminal window
# Installer pip-audit (une seule fois)
pip install pip-audit

Scanner votre environnement actuel :

Terminal window
pip-audit

Exemple de sortie :

Found 2 known vulnerabilities in 1 package
Name Version ID Fix Versions
------- ------- ------------------- ------------
requests 2.25.1 GHSA-j8r2-6x86-q33q 2.31.0
requests 2.25.1 CVE-2023-32681 2.31.0

Scanner un fichier requirements.txt (avant installation) :

Terminal window
pip-audit -r requirements.txt

C’est utile en CI/CD pour bloquer un déploiement si une dépendance vulnérable est détectée.

Éviter l’exécution de code à l’installation

Quand vous installez un package Python, pip peut exécuter setup.py — un script Python arbitraire. Les attaquants exploitent ce mécanisme pour exécuter du code malveillant.

Solution : préférer les wheels (packages pré-compilés).

Un wheel (.whl) est un package déjà compilé. Il n’y a pas de setup.py à exécuter, donc pas de risque d’exécution de code.

Terminal window
# Forcer l'installation uniquement via wheels
pip install --only-binary :all: requests
Type de packageExtensionExécute du code ?
Source (sdist).tar.gz⚠️ Oui (setup.py)
Wheel.whl✅ Non

Limitation : tous les packages n’ont pas de wheel disponible (notamment ceux avec du code C). Dans ce cas, l’installation échouera. Auditez manuellement ces packages avant de les autoriser.

Configurer le registre privé

Pour les packages internes, configurez pip pour utiliser votre registre privé. Créez le fichier pip.conf :

  • Linux : ~/.config/pip/pip.conf
  • macOS : ~/Library/Application Support/pip/pip.conf
  • Windows : %APPDATA%\pip\pip.ini
[global]
# Registre principal : votre registre privé
index-url = https://pypi.acme-corp.com/simple/
# Fallback sur PyPI public pour les packages open source
extra-index-url = https://pypi.org/simple/

Épingler avec hash (intégrité garantie)

Épingler une version (requests==2.31.0) ne suffit pas : un attaquant qui compromettrait PyPI pourrait remplacer le fichier tout en gardant le même numéro de version.

Solution : épingler avec le hash SHA256 du fichier.

requirements.txt
requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f

Avec cette configuration, pip refuse d’installer le package si son hash ne correspond pas. Même si PyPI est compromis, vous êtes protégé.

Générer automatiquement les hashes avec pip-compile :

Terminal window
# Installer pip-tools
pip install pip-tools
# Générer requirements.txt avec hashes depuis requirements.in
pip-compile --generate-hashes requirements.in

Contenu de requirements.in (vos dépendances directes) :

requests
flask

Résultat dans requirements.txt (généré automatiquement) :

requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f
flask==3.0.0 \
--hash=sha256:...
# + toutes les dépendances transitives avec leurs hashes

Protection Go modules

Go a été conçu avec la sécurité de la supply chain en tête. Contrairement à npm et pip, Go n’exécute aucun code lors de l’installation des dépendances. De plus, Google maintient une base de données publique des checksums de tous les modules Go.

Pourquoi Go est plus sûr par design :

Risquenpm/pipGo
Scripts à l’installation⚠️ Oui (postinstall, setup.py)✅ Non
Vérification d’intégrité⚠️ Optionnelle✅ Automatique
Base de checksums centrale❌ Nonsum.golang.org
Proxy cache officiel❌ Nonproxy.golang.org

Vérifier l’intégrité des modules

Le fichier go.sum contient les checksums de tous vos modules. La commande go mod verify vérifie que les modules dans votre cache correspondent à ces checksums.

Terminal window
go mod verify

Sortie attendue si tout va bien :

all modules verified

Sortie en cas de problème :

github.com/example/module v1.2.3: zip has been modified

Cela signifie que le module dans votre cache local a été altéré (attaque ou corruption). Supprimez votre cache et retéléchargez :

Terminal window
go clean -modcache
go mod download

Règle absolue : toujours commiter go.sum.

Terminal window
git add go.sum
git commit -m "chore: update go.sum"

Proxy et base de checksums (configuration par défaut)

Go utilise par défaut le proxy et la base de checksums de Google. Cette configuration est recommandée et ne devrait pas être modifiée sauf cas spécifique.

Terminal window
# Vérifier votre configuration actuelle
go env GOPROXY GOSUMDB

Configuration par défaut (recommandée) :

Terminal window
GOPROXY=https://proxy.golang.org,direct
GOSUMDB=sum.golang.org

Ce que ça signifie :

VariableValeurRôle
GOPROXYproxy.golang.org,directTélécharge via le proxy Google, puis directement si indisponible
GOSUMDBsum.golang.orgVérifie les checksums contre la base Google

Avantages du proxy Google :

  • Cache mondial : téléchargements plus rapides
  • Immuabilité : une version ne peut jamais être modifiée après publication
  • Disponibilité : fonctionne même si GitHub est down

Modules privés (exclure du proxy public)

Pour vos modules internes, vous ne voulez pas que Go essaie de les télécharger via le proxy public (ils n’y sont pas, et vous exposeriez leurs noms).

Terminal window
# Dans votre .bashrc ou .zshrc
export GOPRIVATE=github.com/acme-corp/*,gitlab.acme.com/*

Ce que ça change :

ModuleAvec GOPRIVATESans GOPRIVATE
github.com/acme-corp/utilsTéléchargé directement (git clone)⚠️ Erreur 404 sur proxy.golang.org
github.com/gin-gonic/ginVia proxy.golang.orgVia proxy.golang.org

Pour aller plus loin : si vous avez un proxy Go privé (Athens, Artifactory), combinez GOPRIVATE et GOPROXY :

Terminal window
export GOPROXY=https://goproxy.acme.com,https://proxy.golang.org,direct
export GOPRIVATE=github.com/acme-corp/*

Vérifier la provenance des dépendances

Les protections précédentes (lockfile, registre privé, désactivation des scripts) sont défensives : elles réduisent la surface d’attaque. Mais elles ne répondent pas à une question fondamentale :

Ce package a-t-il été construit à partir du code source que je peux auditer ?

Un mainteneur malveillant pourrait publier un binaire différent du code source GitHub. Le code sur GitHub est propre, mais le package npm/PyPI contient une backdoor. C’est exactement ce qui s’est passé avec xz-utils en 2024.

Qu’est-ce que la provenance SLSA ?

SLSA (Supply-chain Levels for Software Artifacts, prononcé “salsa”) est un framework qui garantit la traçabilité d’un artefact jusqu’à son code source.

Une attestation SLSA prouve :

InformationCe que ça garantit
SourceLe code vient bien de ce dépôt GitHub
BuilderConstruit par GitHub Actions (pas un laptop)
IntégritéLe binaire correspond exactement à ce commit
Non-falsifiableL’attestation est signée cryptographiquement

Analogie : c’est comme un certificat de traçabilité alimentaire. Vous pouvez remonter du steak dans votre assiette jusqu’à la ferme d’origine.

npm : vérifier les signatures de packages

Depuis npm v8.21 (2022), npm génère automatiquement des attestations SLSA pour les packages publiés via GitHub Actions. Lors de l’installation, npm vérifie ces signatures en arrière-plan.

Vérifier que votre npm utilise le bon registre :

Terminal window
npm config get registry

Vous devez obtenir https://registry.npmjs.org/. Si ce n’est pas le cas, les signatures ne seront pas vérifiées.

Auditer les signatures de vos dépendances installées :

Terminal window
npm audit signatures

Exemple de sortie (tout va bien) :

audited 542 packages in 3s
542 packages have verified registry signatures

Exemple de sortie (packages non signés) :

audited 542 packages in 3s
497 packages have verified registry signatures
45 packages have unverified signatures or missing attestations

Les packages sans signature ne sont pas bloqués — tous les packages ne sont pas encore signés. Mais c’est un indicateur : pour vos dépendances critiques, préférez les packages signés.

Go : vérifier avec slsa-verifier

Go n’intègre pas encore nativement la vérification SLSA, mais de plus en plus de projets Go publient des attestations de provenance. Ces attestations prouvent que le binaire a été généré par GitHub Actions à partir d’un commit spécifique.

Workflow de vérification :

  1. Téléchargez le binaire ET son fichier d’attestation (.intoto.jsonl)
  2. Vérifiez que l’attestation est signée par un builder SLSA reconnu
  3. Vérifiez que le binaire correspond au commit attendu

Installation de slsa-verifier :

Terminal window
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest

Exemple concret : vérifier le binaire cosign

Cosign est l’outil de signature d’images du projet Sigstore. Voici comment vérifier que le binaire que vous téléchargez est authentique :

Terminal window
# 1. Télécharger le binaire et son attestation
wget https://github.com/sigstore/cosign/releases/download/v2.4.0/cosign-linux-amd64
wget https://github.com/sigstore/cosign/releases/download/v2.4.0/cosign-linux-amd64.intoto.jsonl
# 2. Vérifier la provenance SLSA
slsa-verifier verify-artifact cosign-linux-amd64 \
--provenance-path cosign-linux-amd64.intoto.jsonl \
--source-uri github.com/sigstore/cosign \
--source-tag v2.4.0

Détail des options :

OptionSignification
verify-artifactMode vérification d’un fichier (vs. image container)
--provenance-pathChemin vers le fichier d’attestation in-toto
--source-uriDépôt GitHub attendu
--source-tagTag git attendu (commit exact)

Sortie en cas de succès :

Verified signature against tlog entry index 12345678
Verified build using builder https://github.com/slsa-framework/slsa-github-generator
PASSED: SLSA verification passed

Sortie en cas d’échec :

FAILED: SLSA verification failed: source-uri mismatch

Si la vérification échoue, ne faites pas confiance au binaire. Quelqu’un a peut-être modifié le fichier ou l’attestation est forgée.

PyPI : Trusted Publishers

PyPI a introduit les Trusted Publishers en 2023. Cette fonctionnalité permet aux mainteneurs de lier leur package à un workflow GitHub Actions spécifique. Seul ce workflow peut publier de nouvelles versions — même le mainteneur ne peut pas publier depuis son laptop.

Comment ça fonctionne :

  1. Le mainteneur configure son projet PyPI pour accepter uniquement les publications venant de github.com/owner/repo/.github/workflows/release.yml
  2. Quand le workflow s’exécute, GitHub génère un token OIDC signé
  3. PyPI vérifie que le token vient bien du workflow autorisé
  4. La publication est acceptée ou refusée

Avantages pour vous (consommateur) :

RisqueSans Trusted PublishersAvec Trusted Publishers
Mainteneur compromis⚠️ Peut publier du malware✅ Ne peut pas (pas de token)
Vol de credentials PyPI⚠️ Publication malveillante possible✅ Le token est éphémère
Build local malveillant⚠️ Possible✅ Seul le CI peut publier

Comment vérifier qu’un package utilise Trusted Publishers :

  1. Allez sur la page PyPI du package (ex: https://pypi.org/project/requests/)
  2. Cliquez sur “Release history” dans la barre latérale
  3. Les releases publiées via Trusted Publishers affichent une icône de vérification ✓ avec le texte “Published via GitHub Actions”

Exemple de packages populaires avec Trusted Publishers :

  • requests (depuis 2024)
  • pydantic
  • httpx
  • ruff

Critères pour choisir une dépendance sécurisée

Ajouter une dépendance n’est pas anodin : c’est du code que vous n’avez pas écrit qui s’exécute avec les mêmes privilèges que votre application. Avant d’ajouter une nouvelle dépendance, évaluez-la sur ces critères.

Checklist de sécurité :

CritèreComment vérifierSeuil recommandéPourquoi c’est important
Signature SLSAnpm audit signatures / slsa-verifierPrésenteGarantit la traçabilité du build
Score Scorecardscorecard --repo=...> 5/10Évalue les pratiques sécurité
Dernière releasePage GitHub/npm/PyPI< 6 moisProjet actif = vulnérabilités corrigées
Mainteneurs actifsContributors sur GitHub≥ 2Bus factor : que faire si le mainteneur disparaît ?
Réponse aux CVESecurity advisories< 30 joursCapacité à réagir aux incidents
Branch protectionScorecard / audit manuelActivéeEmpêche les commits malveillants directs

Exemple : évaluer express avant de l’ajouter

Terminal window
# Vérifier le score Scorecard
scorecard --repo=github.com/expressjs/express

Sortie typique :

Starting [expressjs/express]
Aggregated Score: 6.8/10
Score Check Reason
---- ----- ------
10/10 Code-Review All commits reviewed
7/10 Branch-Protection Branch protection on default branch
9/10 Dependency-Update Dependabot or Renovate active
8/10 Maintained 22 commits in last 90 days
0/10 SAST No SAST tools detected
10/10 Signed-Releases All releases signed

Comment interpréter :

  • Score > 5/10 : acceptable pour la plupart des projets
  • Score < 3/10 : cherchez une alternative
  • Vérifiez spécifiquement Signed-Releases et Branch-Protection
Terminal window
# Exemple : évaluer express avant de l'ajouter
scorecard --repo=github.com/expressjs/express

Bonnes pratiques universelles

  1. Toujours commiter le lockfile (package-lock.json, go.sum, etc.)

  2. Utiliser ci en production (pas install)

  3. Auditer régulièrement (npm audit, pip-audit, go mod verify)

  4. Registre privé pour les packages internes avec priorité explicite

  5. Désactiver les scripts ou les auditer manuellement

  6. Vérifier les mainteneurs avant d’adopter un nouveau package

Outils de détection

OutilÉcosystèmeDétecte
npqnpmTyposquatting, scripts suspects
pip-auditPyPIVulnérabilités connues
Socketnpm, PyPIMalware, comportement suspect
Semgrep Supply ChainMultiPackages malveillants connus
SnykMultiVulnérabilités, license

À retenir

  1. Typosquatting : une faute de frappe = code malveillant
  2. Dependency confusion : registre privé doit avoir priorité explicite
  3. Scripts d’installation : désactiver ou auditer
  4. Lockfile : toujours commiter, utiliser ci en production
  5. Go : plus sécurisé par design (pas de scripts, checksums natifs)
  6. Provenance SLSA : vérifiez que vos dépendances critiques sont signées
  7. Scorecard : évaluez la maturité sécurité avant d’adopter un package

Liens utiles

Ressources externes