Vos workflows d’auto-merge Dependabot et Renovate sont probablement vulnérables. Ce n’est pas de la théorie : en 2025-2026, des chercheurs en sécurité de BoostSecurity, Compass Security et Synacktiv ont publié des attaques concrètes exploitant ces bots mal configurés. L’incident Kong Ingress Controller (décembre 2024) a montré que ces failles sont déjà exploitées en production. Dans cet article, je détaille 6 risques concrets, validés dans un lab avec poutine et zizmor, et je fournis les configurations durcies pour chaque cas.
Pourquoi les bots de dépendances sont une cible
Les bots comme Dependabot et Renovate disposent de privilèges élevés dans
votre pipeline : ils accèdent à GITHUB_TOKEN avec permissions d’écriture, ils
créent des branches, ils peuvent déclencher des workflows. Un attaquant qui
parvient à manipuler le comportement du bot hérite de ces privilèges sans jamais
avoir besoin d’un accès direct au dépôt.
Le problème fondamental : vos workflows font confiance au bot, mais le bot peut être manipulé par un tiers.
Risque n°1 — Confused Deputy : quand github.actor trahit
Le problème
Le pattern le plus répandu pour l’auto-merge Dependabot utilise
github.actor == 'dependabot[bot]' :
# ❌ VULNÉRABLE — github.actor est le DERNIER acteur, pas le créateur de la PRon: pull_request_target: types: [opened, synchronize]
jobs: auto-merge: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' permissions: contents: write pull-requests: write steps: - env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr merge "${{ github.event.pull_request.html_url }}" --auto --mergeLe piège : github.actor représente le dernier utilisateur ayant interagi
avec l’événement, pas le créateur de la PR. Quand un attaquant commente
@dependabot recreate sur une PR Dependabot, Dependabot ferme et recrée la
PR. À ce moment, github.actor vaut dependabot[bot], mais l’attaquant a pu
modifier le contenu entre-temps.
L’attaque (Confused Deputy)
Documentée par BoostSecurity en 2025, l’attaque fonctionne ainsi :
- L’attaquant forke le dépôt cible
- Il crée un conflit de merge intentionnel sur une dépendance
- Il commente
@dependabot recreatesur la PR Dependabot - Dependabot ferme la PR et en recrée une nouvelle
github.actor=dependabot[bot]→ le workflow d’auto-merge se déclenche- Le workflow merge du code que l’attaquant a influencé
La correction
# ✅ SÉCURISÉ — Vérification complèteon: pull_request_target: types: [opened, synchronize]
# Permissions minimales au niveau workflowpermissions: {}
jobs: auto-merge: runs-on: ubuntu-latest permissions: contents: write pull-requests: write # ✅ Vérifier user.login (créateur immuable) + même repo (pas un fork) if: >- github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name steps: # ✅ Utiliser fetch-metadata pour vérifier le type de mise à jour - uses: dependabot/fetch-metadata@d7267c75893950c3e36e1b6a9ec67aa3efef5da4 # v2.3.0 id: metadata
# ✅ Bloquer les mises à jour majeures - if: steps.metadata.outputs.update-type != 'version-update:semver-major' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr merge "${{ github.event.pull_request.html_url }}" --auto --mergeDifférences clés :
user.loginau lieu deactor: identifie le créateur de la PR, pas le dernier intervenant- Vérification du repo : rejette les PR provenant de forks
dependabot/fetch-metadata: valide le type semver avant mergepermissions: {}au niveau workflow : principe du moindre privilège
Ce que poutine détecte
Le scan poutine du lab remonte immédiatement cette vulnérabilité :
Rule: Confused Deputy Auto-MergeSeverity: errorDescription: Confused Deputy for GitHub Actions is a situation where a GitHubevent attribute (ex. github.actor) is used to check the last interaction ofa certain event. This allows an attacker abuse an event triggered by a Bot(ex. @dependabot recreate) and trigger as a side effect other privilegedworkflows, which may for instance automatically merge unapproved changes.Risque n°2 — Injection via le nom de branche Dependabot
Le problème
Dependabot crée des branches avec un nom prévisible :
dependabot/npm_and_yarn/lodash-4.17.21. Ce nom de branche se retrouve dans
github.event.pull_request.head.ref. Si votre workflow interpole cette valeur
dans un bloc run:, c’est une injection de commandes :
# ❌ VULNÉRABLE — Interpolation directe du nom de branchesteps: - name: label run: | echo "Updating dependency from branch: \ ${{ github.event.pull_request.head.ref }}"L’attaque (Merge Conflict Tango)
Documentée par BoostSecurity sous le nom “Merge Conflict Tango” :
- L’attaquant crée un conflit de merge sur le fichier de dépendance
- Il modifie le
package.jsond’une manière qui crée un nom de branche contenant du code injecté - Quand Dependabot recrée la PR pour résoudre le conflit, le nom de branche contient le payload
- Le workflow interpole
head.ref→ exécution de code arbitraire
Une variante appelée “Default Branch Merge Shuffle” exploite le même mécanisme en renommant la branche par défaut du fork pour manipuler le nom de branche Dependabot.
La correction
# ✅ SÉCURISÉ — Variable d'environnement au lieu d'interpolationsteps: - name: label env: BRANCH_REF: ${{ github.event.pull_request.head.ref }} run: | # La variable est traitée comme une valeur, pas comme du code echo "Updating dependency from branch: $BRANCH_REF"Ce que zizmor détecte
error[template-injection]: code injection via template expansion --> .github/workflows/03-vulnerable-branch-injection.yml:22:54 |21 | run: | | --- this run block22 | echo "Updating dependency from branch: \ | ${{ github.event.pull_request.head.ref }}" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | may expand into attacker-controllable codeRisque n°3 — Bypass de la Branch Protection
Le problème
Quand Dependabot est dans la liste de bypass des branch protection rules,
un checkout du code de la PR suivi d’une exécution (npm install, npm test)
donne à l’attaquant une exécution de code arbitraire avec les permissions
du workflow :
# ❌ VULNÉRABLE — Checkout du code de la PR + exécutionsteps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }}
# ❌ npm install exécute les scripts du package.json de la PR - run: npm install - run: npm testL’attaque
- L’attaquant forke le dépôt et modifie le
package.jsonpour ajouter un scriptpreinstallmalveillant - Il ouvre une PR ou utilise la technique du Confused Deputy
- Le workflow checkout le code de la PR (pas celui de la branche principale)
npm installexécute le scriptpreinstall→ exfiltration de secrets
Ce que poutine détecte
Rule: Arbitrary Code Execution from Untrusted Code ChangesSeverity: errorDescription: The workflow appears to checkout untrusted code from a forkand uses a command that is known to allow code execution.Detected usage of `npm`Risque n°4 — TOCTOU : le commit fantôme entre le commentaire et le fetch
Le problème
Le pattern “bot comment trigger” est courant : un mainteneur commente
/ok to test sur une PR de fork, un bot copie le code du fork dans une
branche in-repo et lance la CI avec les secrets.
Le problème est une Time-of-Check to Time-of-Use (TOCTOU) : entre le moment où le mainteneur approuve (commentaire) et le moment où le bot fetch le code (secondes plus tard), l’attaquant peut pousser un commit malveillant.
L’attaque (Bot-Delegated TOCTOU)
Documentée par BoostSecurity en novembre 2025, cette technique a touché de grands projets :
- NVIDIA copy-pr-bot : une fenêtre de ~30 secondes permettait de pousser un commit entre le trigger et le fetch
- GitHub Copilot Extensions : le bot
safe-prcopiait le HEAD flottant de la PR sans vérifier le SHA - Jupyter Notebook : un TOCTOU-dans-un-TOCTOU où un premier commit inoffensif passait la review, puis un second commit malveillant était poussé avant le merge
# ❌ VULNÉRABLE — ref: flottant, pas de SHA fixe- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Le HEAD peut changer entre le commentaire et ce checkout ref: refs/pull/${{ github.event.issue.number }}/headLa correction
# ✅ SÉCURISÉ — Récupérer et verrouiller le SHA exact- name: Get PR SHA id: pr env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}) SHA=$(echo "$PR_DATA" | jq -r '.head.sha') REPO=$(echo "$PR_DATA" | jq -r '.head.repo.full_name') echo "sha=$SHA" >> "$GITHUB_OUTPUT" echo "repo=$REPO" >> "$GITHUB_OUTPUT"
# ✅ Rejeter les forks- name: Reject forks if: steps.pr.outputs.repo != github.repository run: exit 1
# ✅ Checkout du SHA exact, immuable- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ steps.pr.outputs.sha }}Risque n°5 — Renovate self-hosted : autodiscovery + scripts = RCE
Le problème
Renovate self-hosted avec une configuration trop permissive peut transformer le bot en vecteur d’exécution de code à distance (RCE). Compass Security a documenté ces risques en mai 2025.
Voici les options dangereuses :
| Option | Risque |
|---|---|
autodiscover: true sans autodiscoverFilter | Le bot scanne tous les repos accessibles — un attaquant invite le bot dans un repo piégé |
allowScripts: true | Les scripts npm (prepare, postinstall) s’exécutent dans le contexte du runner |
exposeAllEnv: true | Toutes les variables d’environnement (dont RENOVATE_TOKEN) sont accessibles par les scripts |
allowedPostUpgradeCommands: [".*"] | Exécution de commandes arbitraires via postUpgradeTasks |
L’attaque (Autodiscovery Abuse)
- L’attaquant crée un dépôt avec un
package.jsoncontenant un scriptpreparemalveillant - Il invite le bot Renovate (via GitHub App ou accès organisation)
- Renovate scanne le dépôt (autodiscovery), détecte les dépendances
- Lors de la mise à jour,
npm installexécute le scriptprepare - Le script exfiltre
RENOVATE_TOKENet les secrets du runner
{ "// ❌ NE PAS UTILISER EN PRODUCTION": "", "autodiscover": true, "allowScripts": true, "exposeAllEnv": true, "allowedPostUpgradeCommands": [".*"]}La correction
{ "extends": ["config:best-practices"], "autodiscover": true, "autodiscoverFilter": ["my-org/frontend-*", "my-org/backend-*"], "allowScripts": false, "exposeAllEnv": false, "ignoreScripts": true, "allowedPostUpgradeCommands": [], "internalChecksFilter": "strict", "minimumReleaseAge": "3 days"}Points essentiels :
autodiscoverFilter: restreint les repos scannés à un pattern précisignoreScripts: true: bloque les scripts npmexposeAllEnv: false: isole les variables d’environnementinternalChecksFilter: "strict": ne met à jour que les dépendances dont les checks internes Renovate passent
Risque n°6 — Auto-merge sans cooldown : la “golden hour”
Le problème
Quand un paquet npm est compromis (mainteneur piraté, typosquattage, publication malveillante), il existe une fenêtre de temps — la “golden hour” — entre la publication et la détection. Si votre bot de dépendances merge automatiquement sans délai, votre code intègre la version compromise avant que quiconque ne donne l’alerte.
Cas concret : l’incident Nx (2025)
En 2025, un mainteneur du framework Nx a vu son compte npm compromis. L’attaquant a publié une version malveillante. 5 heures se sont écoulées entre la publication et la révocation. Durant cette fenêtre, tous les projets avec auto-merge sans cooldown ont intégré la version compromise.
La solution : le cooldown
Le cooldown (ou minimumReleaseAge pour Renovate) retarde l’ouverture de la PR
de N jours après la publication du paquet. Cela laisse le temps à la communauté,
aux outils de détection et aux mainteneurs de repérer un problème.
Dependabot (GA mi-2025) :
version: 2updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" day: "monday" # ✅ Cooldown : retarder les PRs après publication cooldown: default: 3 # 3 jours pour minor/patch semver-major: 7 # 7 jours pour les majeuresRenovate (via minimumReleaseAge, anciennement stabilityDays) :
{ "extends": ["config:best-practices"], "packageRules": [ { "matchUpdateTypes": ["major"], "minimumReleaseAge": "7 days", "automerge": false }, { "matchUpdateTypes": ["minor", "patch"], "minimumReleaseAge": "3 days", "automerge": true } ]}Le lab : tout tester soi-même
J’ai créé un dépôt lab contenant 8 workflows (5 vulnérables, 3 sécurisés) et 4 configurations (Dependabot + Renovate) pour reproduire tous les scénarios décrits dans cet article.
Scan avec poutine
poutine analyze_local .Résultat sur les workflows vulnérables :
┌────────────────────────────────────────────┬──────────┬────────┐│ RULE ID │ FAILURES │ STATUS │├────────────────────────────────────────────┼──────────┼────────┤│ confused_deputy_auto_merge │ 2 │ Failed ││ default_permissions_on_risky_events │ 2 │ Failed ││ injection │ 2 │ Failed ││ untrusted_checkout_exec │ 3 │ Failed │└────────────────────────────────────────────┴──────────┴────────┘Scan avec zizmor
zizmor .Résultat :
error[bot-conditions]: spoofable bot actor check --> 01-vulnerable-automerge-actor.yml:16:9 |16 | if: github.actor == 'dependabot[bot]' | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ actor context may be spoofable
error[template-injection]: code injection via template expansion --> 03-vulnerable-branch-injection.yml:22:54 |22 | ${{ github.event.pull_request.head.ref }} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | may expand into attacker-controllable codepoutine excelle sur les patterns spécifiques aux bots (confused_deputy,
untrusted_checkout_exec), tandis que zizmor est plus fort sur l’analyse
statique générale (template-injection, bot-conditions). Les deux sont
complémentaires.
Checklist de durcissement
| # | Vérification | Outil |
|---|---|---|
| 1 | github.event.pull_request.user.login au lieu de github.actor | poutine, zizmor |
| 2 | Vérifier github.repository == head.repo.full_name (anti-fork) | Manuel |
| 3 | dependabot/fetch-metadata pour valider le type semver | Manuel |
| 4 | Pas d’interpolation ${{ }} dans run: avec des valeurs contrôlées par l’utilisateur | zizmor |
| 5 | permissions: {} au niveau workflow, permissions minimales par job | poutine, zizmor |
| 6 | SHA-pinning exact pour les checkout sur commentaire trigger | Manuel |
| 7 | autodiscoverFilter pour Renovate self-hosted | Manuel |
| 8 | ignoreScripts: true + exposeAllEnv: false | Manuel |
| 9 | Cooldown activé (Dependabot) ou minimumReleaseAge (Renovate) | Manuel |
| 10 | Pas de npm install/pip install sur du code non approuvé | poutine |
À retenir
Les bots de dépendances ne sont pas magiquement sûrs. Dependabot et Renovate héritent des permissions de votre pipeline. Mal configurés, ils deviennent le maillon faible que les attaquants exploitent.
Les 3 principes fondamentaux :
- Vérifier l’identité :
user.login, pasactor. Toujours vérifier que la PR vient du même repo. - Limiter l’exécution : ne jamais exécuter de code provenant d’une PR non
approuvée. Utiliser
ignoreScriptspour Renovate. - Ralentir l’auto-merge : le cooldown n’est pas un luxe, c’est une défense contre les compromissions de paquets. 3 jours minimum, 7 pour les majeures.
Scannez vos workflows avec poutine et zizmor : en 30 secondes, vous saurez si vos pipelines sont exposés.
Sources
- Weaponizing Dependabot: Pwn Request at its Finest — BoostSecurity, 2025
- Split-Second Side Doors: Bot-Delegated TOCTOU — BoostSecurity, novembre 2025
- Defensive Research, Weaponized: 2025 State of Pipeline Security — BoostSecurity, décembre 2025
- Renovate – Keeping Your Updates Secure? — Compass Security, mai 2025
- Dependency cooldowns: a simple supply chain fix — Christian Schneider, janvier 2026
- Dependabot cooldown period — GitHub Blog, 2025
- Guide poutine — Audit sécurité des pipelines CI/CD — blog.stephane-robert.info
- Guide zizmor — Audit statique des workflows GitHub Actions — blog.stephane-robert.info
- GitHub Actions : 15 pièges de sécurité qui exposent vos pipelines — blog.stephane-robert.info