Aller au contenu
medium

Dependabot et Renovate : 6 failles quand ils sont mal configurés

20 min de lecture

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]' :

.github/workflows/auto-merge.yml
# ❌ VULNÉRABLE — github.actor est le DERNIER acteur, pas le créateur de la PR
on:
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 --merge

Le 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 :

  1. L’attaquant forke le dépôt cible
  2. Il crée un conflit de merge intentionnel sur une dépendance
  3. Il commente @dependabot recreate sur la PR Dependabot
  4. Dependabot ferme la PR et en recrée une nouvelle
  5. github.actor = dependabot[bot] → le workflow d’auto-merge se déclenche
  6. Le workflow merge du code que l’attaquant a influencé

La correction

.github/workflows/auto-merge-secure.yml
# ✅ SÉCURISÉ — Vérification complète
on:
pull_request_target:
types: [opened, synchronize]
# Permissions minimales au niveau workflow
permissions: {}
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 --merge

Différences clés :

  • user.login au lieu de actor : 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 merge
  • permissions: {} 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-Merge
Severity: error
Description: Confused Deputy for GitHub Actions is a situation where a GitHub
event attribute (ex. github.actor) is used to check the last interaction of
a certain event. This allows an attacker abuse an event triggered by a Bot
(ex. @dependabot recreate) and trigger as a side effect other privileged
workflows, 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 :

.github/workflows/label-pr.yml
# ❌ VULNÉRABLE — Interpolation directe du nom de branche
steps:
- 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” :

  1. L’attaquant crée un conflit de merge sur le fichier de dépendance
  2. Il modifie le package.json d’une manière qui crée un nom de branche contenant du code injecté
  3. Quand Dependabot recrée la PR pour résoudre le conflit, le nom de branche contient le payload
  4. 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

.github/workflows/label-pr-secure.yml
# ✅ SÉCURISÉ — Variable d'environnement au lieu d'interpolation
steps:
- 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 block
22 | echo "Updating dependency from branch: \
| ${{ github.event.pull_request.head.ref }}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| may expand into attacker-controllable code

Risque 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 :

.github/workflows/ci-dependabot.yml
# ❌ VULNÉRABLE — Checkout du code de la PR + exécution
steps:
- 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 test

L’attaque

  1. L’attaquant forke le dépôt et modifie le package.json pour ajouter un script preinstall malveillant
  2. Il ouvre une PR ou utilise la technique du Confused Deputy
  3. Le workflow checkout le code de la PR (pas celui de la branche principale)
  4. npm install exécute le script preinstall → exfiltration de secrets

Ce que poutine détecte

Rule: Arbitrary Code Execution from Untrusted Code Changes
Severity: error
Description: The workflow appears to checkout untrusted code from a fork
and 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-pr copiait 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
.github/workflows/toctou-vulnerable.yml
# ❌ 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 }}/head

La correction

.github/workflows/toctou-secure.yml
# ✅ 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 :

OptionRisque
autodiscover: true sans autodiscoverFilterLe bot scanne tous les repos accessibles — un attaquant invite le bot dans un repo piégé
allowScripts: trueLes scripts npm (prepare, postinstall) s’exécutent dans le contexte du runner
exposeAllEnv: trueToutes les variables d’environnement (dont RENOVATE_TOKEN) sont accessibles par les scripts
allowedPostUpgradeCommands: [".*"]Exécution de commandes arbitraires via postUpgradeTasks

L’attaque (Autodiscovery Abuse)

  1. L’attaquant crée un dépôt avec un package.json contenant un script prepare malveillant
  2. Il invite le bot Renovate (via GitHub App ou accès organisation)
  3. Renovate scanne le dépôt (autodiscovery), détecte les dépendances
  4. Lors de la mise à jour, npm install exécute le script prepare
  5. Le script exfiltre RENOVATE_TOKEN et les secrets du runner
renovate-vulnerable.json
{
"// ❌ NE PAS UTILISER EN PRODUCTION": "",
"autodiscover": true,
"allowScripts": true,
"exposeAllEnv": true,
"allowedPostUpgradeCommands": [".*"]
}

La correction

renovate-secure.json
{
"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écis
  • ignoreScripts: true : bloque les scripts npm
  • exposeAllEnv: false : isole les variables d’environnement
  • internalChecksFilter: "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) :

.github/dependabot.yml
version: 2
updates:
- 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 majeures

Renovate (via minimumReleaseAge, anciennement stabilityDays) :

renovate.json
{
"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

Fenêtre de terminal
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

Fenêtre de terminal
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 code

poutine 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érificationOutil
1github.event.pull_request.user.login au lieu de github.actorpoutine, zizmor
2Vérifier github.repository == head.repo.full_name (anti-fork)Manuel
3dependabot/fetch-metadata pour valider le type semverManuel
4Pas d’interpolation ${{ }} dans run: avec des valeurs contrôlées par l’utilisateurzizmor
5permissions: {} au niveau workflow, permissions minimales par jobpoutine, zizmor
6SHA-pinning exact pour les checkout sur commentaire triggerManuel
7autodiscoverFilter pour Renovate self-hostedManuel
8ignoreScripts: true + exposeAllEnv: falseManuel
9Cooldown activé (Dependabot) ou minimumReleaseAge (Renovate)Manuel
10Pas 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 :

  1. Vérifier l’identité : user.login, pas actor. Toujours vérifier que la PR vient du même repo.
  2. Limiter l’exécution : ne jamais exécuter de code provenant d’une PR non approuvée. Utiliser ignoreScripts pour Renovate.
  3. 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

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