L'événement pull_request_target est l'un des plus dangereux de GitHub
Actions. Il exécute du code avec accès aux secrets, même pour les PR de
forks. Mal utilisé, il ouvre la porte à l'exfiltration de vos credentials.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Distinguer
pull_requestetpull_request_targetet leur accès aux secrets - Reconnaître l'attaque classique :
pull_request_target+ checkout du fork - Choisir la bonne garde anti-fork, de la plus forte à la plus faible
- Éviter le piège du faux garde
github.repository == - Appliquer le pattern en deux workflows relié par
workflow_run - Détecter les workflows vulnérables et vérifier si vous avez déjà été exploité
Différence entre pull_request et pull_request_target
Section intitulée « Différence entre pull_request et pull_request_target »Ces deux déclencheurs réagissent à une pull request, mais leur modèle de sécurité est opposé. Comprendre cette différence est la base de tout le reste.
| Événement | Accès aux secrets | Code exécuté | Contexte |
|---|---|---|---|
pull_request | ❌ Non (forks) | Code de la PR | Fork |
pull_request_target | ✅ Oui | Code de la branche cible | Base repo |
pull_request (sûr par défaut)
Section intitulée « pull_request (sûr par défaut) »Avec pull_request, le workflow s'exécute dans le contexte du fork : il
voit le code de la PR mais n'a aucun accès aux secrets. C'est le
comportement sûr par défaut.
on: pull_request: types: [opened, synchronize]
jobs: test: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false # Le code vient de la PR (fork potentiel) # Pas d'accès aux secrets pour les forks = sûr - run: npm testpull_request_target (dangereux)
Section intitulée « pull_request_target (dangereux) »Avec pull_request_target, le workflow s'exécute dans le contexte du dépôt
cible : il a accès aux secrets. Tant qu'il ne checkout pas le code du
fork, cela reste maîtrisé.
on: pull_request_target: types: [opened, synchronize]
jobs: test: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false # Le code vient de la branche cible (main) # Accès aux secrets = OK si on ne checkout pas le code du fork - run: npm testL'attaque classique
Section intitulée « L'attaque classique »Le danger survient quand vous combinez pull_request_target avec un checkout
du code de la PR :
# ❌ DANGEREUX : accès aux secrets + code du forkon: pull_request_target:
jobs: build: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} # Code du fork !
# Ce script vient du fork et a accès aux secrets - run: npm test env: API_TOKEN: ${{ secrets.API_TOKEN }} # Exfiltrable !Scénario d'attaque :
- L'attaquant fork votre repo
- Il modifie
package.jsonpour ajouter un script malveillant danstest - Il ouvre une PR
- Votre workflow
pull_request_targets'exécute - Le script malveillant envoie
$API_TOKENvers un serveur externe
Ce qui aggrave l'exploitation
Section intitulée « Ce qui aggrave l'exploitation »Une fuite de secret est déjà grave. Trois facteurs la transforment en incident bien plus large.
- Runner self-hosted : un
pull_request_targetvulnérable sur un runner self-hosted ne fuite pas seulement un secret, il donne une exécution de code arbitraire sur votre infrastructure. L'attaquant peut persister sur la machine, pivoter vers le réseau interne et contaminer les runs suivants. C'est de la RCE, pas une simple fuite. - OIDC (
id-token: write) : si le job dispose deid-token: writesans garde, l'attaquant récupère un jeton OIDC vers votre cloud. C'est pire qu'un secret de dépôt : il obtient les rôles IAM associés et pivote directement dans votre compte cloud. - Action tierce non épinglée : dans le job privilégié, une action référencée par tag (
@v4) ou une image par tag mutable est la deuxième marche de l'escalade. Épinglez chaque action par SHA (uses: org/action@<sha>) et chaque image par digest (image@sha256:...).
Cas d'usage légitimes
Section intitulée « Cas d'usage légitimes »Le déclencheur n'est pas à bannir : il a des usages légitimes, tous caractérisés par une même propriété, ils n'exécutent jamais le code du fork.
1. Labeling automatique (sans checkout du fork)
Section intitulée « 1. Labeling automatique (sans checkout du fork) »Étiqueter une PR selon les fichiers modifiés ne nécessite que l'API GitHub, jamais le code du fork.
# ✅ Sûr : pas de checkout du code du forkon: pull_request_target: types: [opened]
permissions: {}
jobs: label: runs-on: ubuntu-24.04 permissions: contents: read pull-requests: write steps: - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }}2. Commenter les PRs
Section intitulée « 2. Commenter les PRs »Poster un message de bienvenue passe par actions/github-script, qui appelle
l'API GitHub sans rien cloner.
# ✅ Sûr : pas d'exécution de code du forkon: pull_request_target: types: [opened]
permissions: {}
jobs: welcome: runs-on: ubuntu-24.04 permissions: pull-requests: write steps: - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: 'Merci pour votre contribution !' })Les gardes anti-fork, par robustesse
Section intitulée « Les gardes anti-fork, par robustesse »Quand un pull_request_target doit vraiment tourner sur des PR entrantes, une garde décide quelles PR ont le droit de s'exécuter avec les secrets. Toutes ne se valent pas. Voici les cinq gardes, de la plus forte à la plus faible.
| Garde | Robustesse | Condition (if:) |
|---|---|---|
| Vérifier l'absence de fork | Forte, sans humain | github.event.pull_request.head.repo.fork == false |
| Job amont de permission | Forte | needs.gate.outputs.allowed == 'true' |
environment + reviewers | Forte, approbation manuelle | (pas de if, environment: protégé) |
| Acteur de confiance | Cas des bots | github.actor == 'dependabot[bot]' |
| Label maintainer-only | La plus faible | contains(github.event.pull_request.labels.*.name, 'safe-to-test') |
1. Vérifier que la PR ne vient pas d'un fork (forte)
Section intitulée « 1. Vérifier que la PR ne vient pas d'un fork (forte) »Ne rien exécuter de privilégié si la PR provient d'un fork. C'est la garde la plus nette, sans intervention humaine.
jobs: privileged: if: github.event.pull_request.head.repo.fork == false runs-on: ubuntu-24.04Quand l'utiliser : dès que le job privilégié n'a de sens que pour les PR internes (branches du dépôt lui-même).
2. Job amont de vérification de permission (forte)
Section intitulée « 2. Job amont de vérification de permission (forte) »Un premier job vérifie que l'auteur a un rôle write, et le job privilégié en dépend (needs) en mode fail-closed : si la vérification échoue, le second ne tourne pas.
jobs: gate: runs-on: ubuntu-24.04 outputs: allowed: ${{ steps.check.outputs.require-result }} steps: - uses: actions-cool/check-user-permission@c21884f3dda18dafc2f8b402fe807ccc9ec1aa5e # v2.4.0 id: check with: require: write privileged: needs: gate if: needs.gate.outputs.allowed == 'true' runs-on: ubuntu-24.04Quand l'utiliser : quand des contributeurs de confiance (au-delà des bots) doivent pouvoir déclencher le job.
3. environment avec reviewers requis (forte, approbation manuelle)
Section intitulée « 3. environment avec reviewers requis (forte, approbation manuelle) »Rattacher le job à un environment protégé par des required reviewers. GitHub met le job en pause jusqu'à une approbation humaine.
jobs: privileged: environment: fork-pr # protégé par des required reviewers runs-on: ubuntu-24.04Quand l'utiliser : quand un humain doit relire la PR avant toute exécution privilégiée.
4. Acteur de confiance (cas des bots)
Section intitulée « 4. Acteur de confiance (cas des bots) »N'autoriser qu'un acteur précis, typiquement un bot connu comme Dependabot.
jobs: privileged: if: github.actor == 'dependabot[bot]' runs-on: ubuntu-24.04Quand l'utiliser : pour des PR automatisées d'un bot identifié. À combiner avec une autre garde, github.actor seul reste une condition d'identité, pas de relecture.
5. Label maintainer-only (la plus faible)
Section intitulée « 5. Label maintainer-only (la plus faible) »N'exécuter que si un label a été posé sur la PR.
jobs: privileged: if: contains(github.event.pull_request.labels.*.name, 'safe-to-test') runs-on: ubuntu-24.04Quand l'utiliser : en complément, jamais seule. Un label n'est posable que par un compte write ou triage, mais poser un label n'est pas relire le code : le mainteneur peut labelliser sans avoir audité le diff, et le fork peut pousser un nouveau commit après le label.
Pattern sécurisé : workflow en deux parties
Section intitulée « Pattern sécurisé : workflow en deux parties »Si vous devez exécuter du code du fork et avoir accès aux secrets, séparez le travail en deux workflows distincts.
Workflow 1 : Build sans secrets
Section intitulée « Workflow 1 : Build sans secrets »Le premier workflow se déclenche sur pull_request : il build le code du
fork mais sans aucun secret à voler.
name: PR Build
on: pull_request: # Pas de secrets, code du fork
permissions: {}
jobs: build: runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Récupérer le code de la PR uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Builder le projet run: npm ci && npm run build - name: Publier l'artefact de build uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: build-output path: dist/Workflow 2 : Déploiement avec secrets
Section intitulée « Workflow 2 : Déploiement avec secrets »Le second se déclenche sur workflow_run, une fois le build terminé : il a les
secrets mais ne manipule que l'artefact, jamais le code source du fork.
name: Deploy Preview
on: workflow_run: workflows: ["PR Build"] types: [completed]
permissions: {}
jobs: deploy: if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-24.04 permissions: actions: read # Télécharger l'artefact de l'autre run pull-requests: write # Commenter l'URL de preview steps: # Télécharger l'artefact (pas le code source) - name: Récupérer l'artefact de build uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: build-output path: dist/ run-id: ${{ github.event.workflow_run.id }} github-token: ${{ github.token }} # Déployer avec les secrets - name: Déployer la preview run: ./deploy-preview.sh env: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Pourquoi c'est sûr :
- Le premier workflow exécute le code du fork, mais sans accès aux secrets
- Le second workflow a accès aux secrets, mais ne télécharge que l'artefact (pas le code source du fork)
- L'artefact ne peut pas contenir de code exécutable (juste les fichiers buildés)
Règles de sécurité
Section intitulée « Règles de sécurité »En résumé, la sécurité de pull_request_target tient en une règle, ne
jamais exécuter le code du fork avec les secrets, et trois patterns sûrs.
❌ Ne jamais faire
Section intitulée « ❌ Ne jamais faire »Le combo mortel : checkout du code du fork et secrets exposés dans le même job.
on: pull_request_target
jobs: build: runs-on: ubuntu-24.04 steps: # Checkout du code du fork - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }}
# Exécution de scripts du fork avec secrets - run: ./scripts/build.sh env: SECRET: ${{ secrets.SECRET }}✅ Patterns sûrs
Section intitulée « ✅ Patterns sûrs »Trois patterns couvrent tous les besoins réels sans jamais exécuter le code du fork avec les secrets.
- Pas de checkout du fork :
on: pull_request_target
jobs: label: runs-on: ubuntu-24.04 steps: # Actions GitHub uniquement, pas de code du fork - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0- Checkout de la branche cible uniquement :
on: pull_request_target
jobs: analyze: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Sans ref = checkout de la branche cible (main) - run: ./trusted-script.sh # Script de main, pas du fork- Séparation build/deploy via workflow_run :
on: workflow_run: workflows: ["Untrusted Build"] types: [completed]Le garde-fou de checkout v7
Section intitulée « Le garde-fou de checkout v7 »Bonne nouvelle de mi-2026 : actions/checkout@v7 ajoute enfin une protection
par défaut contre la pwn request. Quand un workflow pull_request_target (ou
certains workflow_run) tente de checkouter la ref du fork (la branche ou le SHA
de la PR), checkout refuse désormais l'opération au lieu de tirer du code non
fiable dans un runner aux permissions élevées.
Ce qu'il faut retenir :
- Le pattern dangereux (
ref: ${{ github.event.pull_request.head.sha }}souspull_request_target) échoue par défaut. C'est exactement l'anti-pattern décrit plus haut, désormais bloqué nativement. - Un nouvel input
allow-unsafe-pr-checkoutréautorise explicitement l'ancien comportement. Traitez-le comme une exception sensible : à justifier, isoler et relire, jamais activé par confort. - Le changement est rétroporté le 16 juillet 2026 sur les versions majeures
supportées ; les tags flottants (
@v4,@v3) héritent automatiquement de la protection.
Source : l'analyse de Socket sur le blocage des checkouts pull_request_target.
Détecter les workflows vulnérables
Section intitulée « Détecter les workflows vulnérables »Plutôt que d'auditer à l'œil, plusieurs scanners repèrent automatiquement les
pull_request_target dangereux.
Avec plumber
Section intitulée « Avec plumber »plumber, le scanner de pipelines CI/CD,
signale ce pattern via le contrôle ISSUE-804 (checkout de code non fiable sous
pull_request_target).
plumber analyze --github-url https://github.com/votre-org/votre-repoLe détail du contrôle et sa remédiation sont documentés ici : ISSUE-804 dans la doc plumber.
Avec Backflow
Section intitulée « Avec Backflow »Là où plumber repère l'odeur (le pattern à risque dans le YAML), Backflow prouve le chemin : il suit le flux de données de l'entrée attaquant (la ref du fork checkoutée) jusqu'au secret exfiltré, déroulé pas à pas. La distinction est utile en pratique : plumber vous dit où regarder, Backflow confirme qu'un chemin exploitable existe réellement et permet de prioriser la remédiation sur les workflows qui aboutissent vraiment à un secret, plutôt que sur les faux positifs.
Avec Scorecard
Section intitulée « Avec Scorecard »Le check Dangerous-Workflow d'OpenSSF Scorecard signale précisément ce pattern.
scorecard --local . --checks Dangerous-Workflow --show-detailsAvec Checkov
Section intitulée « Avec Checkov »Checkov couvre la même règle via l'identifiant CKV_GHA_1.
checkov -d .github/workflows/ --check CKV_GHA_1Pattern à rechercher
Section intitulée « Pattern à rechercher »Pour un audit manuel rapide, deux grep suffisent à lister les workflows à
inspecter de près.
# Chercher les workflows potentiellement dangereuxgrep -r "pull_request_target" .github/workflows/grep -r "github.event.pull_request.head" .github/workflows/Ai-je déjà été exploité ?
Section intitulée « Ai-je déjà été exploité ? »Point crucial souvent ignoré : l'exploitation ne laisse aucune trace dans le dépôt. Un pull_request_target vulnérable s'exécute au moment de la PR, sans merge : la preuve est dans les runs Actions, pas dans l'historique Git. Chercher un commit suspect ne sert donc à rien.
La bonne question est : un fork a-t-il déjà déclenché mon workflow privilégié ? Sous pull_request_target, il suffit de comparer le dépôt source (head_repository) au dépôt de base (repository) de chaque run. L'API GitHub expose ces deux champs (contrairement à gh run list, qui ne les renvoie pas) :
# Runs pull_request_target où la source diffère du dépôt de base = PR de forkgh api "repos/VOTRE_ORG/VOTRE_REPO/actions/runs?event=pull_request_target&per_page=100" \ --jq '.workflow_runs[] | select(.head_repository.full_name != .repository.full_name) | {id, fork: .head_repository.full_name, date: .created_at, workflow: .name}'Fork différent du dépôt de base égale code de fork exécuté avec vos secrets. Si un tel run porte sur un workflow qui checkoutait la ref du fork (head.sha), considérez les secrets comme compromis : rotez-les immédiatement, puis auditez les logs du run (connexions réseau sortantes, variables d'environnement affichées, commandes inhabituelles).
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »pull_requests'exécute dans le contexte du fork sans secrets ;pull_request_targets'exécute dans le dépôt cible avec les secrets.- L'attaque classique combine
pull_request_targetet un checkout du code du fork (ref: ...head.sha), le code de l'attaquant tourne alors avec vos secrets. - Les usages légitimes de
pull_request_targetn'exécutent jamais le code du fork : labelling et commentaires passent par l'API GitHub seule. - Pour builder le code d'un fork et utiliser des secrets, séparez en deux workflows reliés par
workflow_run, le second ne consomme qu'un artefact. - Auditez avec le check Dangerous-Workflow de Scorecard, la règle
CKV_GHA_1de Checkov, ou le contrôle ISSUE-804 de plumber. - Pour exécuter du privilégié sur des PR, préférez une garde forte (absence de fork, job de permission,
environment+ reviewers) ; le label est la plus faible et ne remplace pas une relecture. github.repository ==ne protège pas : souspull_request_target, il vaut toujours le dépôt de base. Distinguez un fork avechead.repo.fork.- L'exploitation ne merge pas : la trace est dans les runs Actions (
head_repositorydifférent du dépôt de base). En cas de doute, rotez les secrets avant même d'analyser.
FAQ : Questions fréquentes
Section intitulée « FAQ : Questions fréquentes »pull_request_target, github.repository vaut toujours le dépôt de base, jamais celui du fork. Une condition if: github.repository == 'org/repo' est donc vraie pour toutes les PR entrantes, y compris celles d'un fork malveillant : elle ne bloque rien. Pour distinguer réellement un fork, testez github.event.pull_request.head.repo.fork == false ou comparez head.repo.full_name à github.repository.pull_request_target et repérez celles dont la source diffère du dépôt de base :gh api "repos/VOTRE_ORG/VOTRE_REPO/actions/runs?event=pull_request_target&per_page=100" \
--jq '.workflow_runs[] | select(.head_repository.full_name != .repository.full_name) | {id, fork: .head_repository.full_name}'
Si un tel run porte sur un workflow qui checkoutait la ref du fork, considérez les secrets comme compromis : rotez-les immédiatement et auditez les logs.environment + reviewers), jamais seul.pull_request (qui tourne dans le contexte du fork, sans secrets), pull_request_target s'exécute dans le contexte du dépôt de base avec accès aux secrets, même pour une PR venant d'un fork. Tant que le workflow n'exécute pas le code du fork, cela reste maîtrisé. Le risque apparaît dès qu'on checkoute la ref du fork (head.sha) puis exécute son code dans ce contexte privilégié.Prochaines étapes
Section intitulée « Prochaines étapes »Pour le détail des vecteurs d'exploitation, voir l'analyse de référence du GitHub Security Lab : Keeping your GitHub Actions secure.