~9 minutes
Le 18 juin 2026, GitHub a publié un changelog qui mérite plus qu’un coup d’œil distrait.
actions/checkout v7 refuse désormais, par défaut, de récupérer le code d’une pull request issue d’un fork dans un workflow déclenché par pull_request_target ou workflow_run. C’est une ligne de changelog. C’est surtout l’aveu officiel qu’un motif d’attaque vieux de plusieurs années, le “pwn request”, a causé assez d’incidents pour justifier un changement de comportement par défaut sur l’action la plus utilisée de toute la plateforme.
J’ai déjà traité ce risque dans mon article sur le durcissement des pipelines CI/CD, où je classais le Poisoned Pipeline Execution comme la menace n°4 de l’OWASP Top 10 CI/CD. Ce que je veux faire ici, c’est repartir de l’annonce GitHub pour remonter au principe général, et surtout sortir du cadre GitHub Actions. Parce que GitLab CI et Jenkins ont strictement le même problème, sous un autre nom et avec un autre bouton à cocher.
Le pwn request, kesako ?
Je définis un pwn request comme l’exécution de code non vérifié, provenant d’un contributeur externe, dans un contexte d’exécution qui dispose des secrets et des privilèges du dépôt cible. La mécanique est presque toujours la même : un trigger de CI conçu pour des cas légitimes (poster un commentaire de résultat de tests sur une PR externe, par exemple) est configuré pour s’exécuter avec les permissions du dépôt principal, et quelqu’un oublie que le code exécuté, lui, vient du fork de l’attaquant. Enfin, oublie, le mot est presque trop poli ; dans la plupart des cas que j’ai vus, personne n’avait jamais fait le lien entre les deux lignes de configuration.
Note du PandaRedacteur : Le secret n’est jamais volé directement. Il est utilisé sur place, par un job qui croit légitimement travailler pour vous.
Il ne faut pas ranger cela dans la case “bug GitHub Actions” : c’est un piège de conception qui existe dans toute plateforme de CI dès qu’un trigger privilégié peut checkouter du contenu non privilégié.
SolarWinds, Codecov, et plus récemment Shai-Hulud sur npm illustrent des variantes de cette même idée : compromettre l’usine plutôt que le produit fini. Je l’avais déjà posé dans La face cachée de la Supply Chain et retrouvé sous l’angle Kubernetes dans K10 : le pipeline CI/CD n’est pas un outil, c’est une cible.
Note du PandaRedacteur : j’ai vu passer trois fois cette année où le coupable était exactement ce job “innocent” qui poste un commentaire de couverture sur les PR. Personne ne l’auditait parce que “ça ne fait que commenter”.
Vecteurs d’attaque
Distinguons ici quatre variantes du même motif, pour montrer qu’il ne s’agit pas d’une particularité GitHub. Quatre, ou trois et un quart si on est honnête, le dernier cas Jenkins recoupe largement le premier, mais je préfère le détailler à part.
pull_request_target avec checkout de la PR
Le workflow se déclenche avec pull_request_target. Cela donne accès aux secrets et au GITHUB_TOKEN en écriture, dès la première ligne du job. Puis une étape fait un actions/checkout en pointant explicitement le SHA ou la ref de la pull request, parce qu’il faut bien lancer les tests ou un linter sur le code proposé par le contributeur.
À partir de là, n’importe quelle commande exécutée pendant ce job (script de build, hook postinstall, étape de test) tourne avec les secrets du dépôt cible sur du code écrit par l’attaquant.
Il y a un cas où je trouve ce trigger légitime et franchement difficile à remplacer : un bot qui doit commenter une PR externe avec des informations qui dépendent du code proposé (couverture, taille du bundle, résultats de benchmark). Pas de bonne alternative agnostique ici, à part accepter le risque et le compenser ailleurs.
Note du PandaRedacteur : dans ce cas précis, je préfère un job minimal, sans accès réseau sortant et sans aucun secret au-delà du token de commentaire scopé, plutôt que de me raconter qu’on va “faire attention”.
workflow_run et l’artefact non vérifié
Un premier workflow, sans privilèges, build et publie un artefact à partir du code de la PR. Rien d’alarmant jusqu’ici. Un second workflow, déclenché par workflow_run et disposant de secrets, télécharge ensuite cet artefact pour le signer ou publier un rapport de couverture, sans jamais revérifier sa provenance.
L’attaquant n’a pas besoin que son code tourne directement dans le contexte privilégié ; il lui suffit que son artefact soit consommé sans contrôle par celui qui en a un.
Pipelines de merge request GitLab CI
GitLab propose les “merge request pipelines” et les “pipelines for merged results”. Tous deux s’exécutent dans le contexte du projet cible, avec ce que cela implique. Si les rules du .gitlab-ci.yml autorisent un job avec des variables protégées à tourner sur une MR ouverte depuis un fork externe, on a recréé pull_request_target avec un nom différent et un faux sentiment de sécurité en prime.
La documentation GitLab elle-même recommande de restreindre l’usage des “protected variables” aux branches protégées, précisément pour cette raison.
Multibranch Pipeline et PR builder sur Jenkins
Avec le plugin GitHub Branch Source ou le PR Builder Plugin, Jenkins peut déclencher un job multibranche directement à partir d’une PR externe. Et il exécute le Jenkinsfile qui se trouve… dans la PR elle-même, écrit par l’attaquant, validé par personne.
Si ce job a accès aux credentials globaux configurés dans Jenkins (et c’est très souvent le cas par défaut), l’attaquant contrôle le pipeline qui dispose des clés.
Exemple concret
Voici comment se déroule un pwn request typique sur un dépôt GitHub qui publie un commentaire de couverture de tests sur chaque PR :
- Le mainteneur configure un workflow
pull_request_targetpour pouvoir poster un commentaire avec les résultats de couverture sur les PR externes, ce qui nécessite leGITHUB_TOKENen écriture. - Pour calculer cette couverture, le workflow fait un
actions/checkoutavecref: $, donc le code exact proposé par le contributeur. - L’attaquant ouvre une PR depuis un fork. Son code modifie un fichier de configuration de test (
jest.config.js,pytest.ini, un hookpostinstalldepackage.json) pour exécuter une commande arbitraire au moment du build. - Le workflow s’exécute automatiquement, dans le contexte du dépôt cible, avec le
GITHUB_TOKENen écriture et les secrets éventuellement exposés à ce job. - La commande de l’attaquant exfiltre les secrets vers un serveur externe, ou pousse un commit signé avec les droits du token, ou crée un nouveau workflow malveillant directement sur le dépôt cible.
- Le mainteneur ne voit rien d’anormal dans la PR : le diff visible touche un fichier de test, pas le pipeline.
Le délai entre l’ouverture de la PR et l’exfiltration complète : le temps d’un job CI, souvent moins de deux minutes.
Analyse STRIDE de ce type d’attaque
| Catégorie STRIDE | Applicable | Explication |
|---|---|---|
| Spoofing (Usurpation d'identité) | Oui | Le code de l'attaquant s'exécute sous l'identité du workflow légitime ; toute action effectuée (commit, commentaire, déploiement) semble venir du dépôt lui-même. |
| Tampering (Falsification) | Oui (Primaire) | C'est le cœur de l'attaque : un fichier de configuration ou un hook de build modifié injecte une commande arbitraire dans un job privilégié. |
| Repudiation (Répudiation) | Modéré | Les logs du job montrent une exécution "normale" déclenchée par une PR ; sans audit fin du contenu exécuté, il est difficile d'attribuer l'action à l'attaquant après coup. |
| Information Disclosure (Divulgation) | Oui | Le GITHUB_TOKEN et les secrets accessibles au job sont exposés à du code arbitraire et peuvent être exfiltrés en une commande curl. |
| Elevation of Privilege (Élévation de privilèges) | Oui | Un contributeur externe sans aucun droit sur le dépôt obtient, via le pipeline, les mêmes privilèges que le GITHUB_TOKEN ou les secrets du job. |
J’ai volontairement retiré le Denial of Service de ce tableau. La catégorie existe sur le papier, mais je n’ai jamais vu cette variante exploitée en pratique sur un pwn request : un attaquant qui a déjà accès aux secrets du dépôt a largement mieux à faire que de saturer des runners.
Impact potentiel
| Impact | Niveau | Description de l'impact |
|---|---|---|
| Confidentialité | Critique | Le job compromis a accès aux secrets du dépôt cible (tokens, clés cloud, credentials de registry) qui peuvent être exfiltrés en une seule commande. |
| Intégrité | Critique | Avec un token en écriture, l'attaquant peut pousser des commits, créer de nouveaux workflows malveillants ou modifier des releases, sous une identité qui semble légitime. |
| Disponibilité | Modéré | La révocation d'urgence des tokens et secrets compromis interrompt les pipelines en cours pour toute l'équipe, le temps de l'investigation. |
| Réputation | Élevé | Un dépôt open source compromis via une PR publique expose directement la chaîne d'approvisionnement de tous ses utilisateurs en aval. |
Une approche agnostique du moteur CI
Je n’ai pas vu beaucoup d’articles sur ce sujet sortir du vocabulaire GitHub Actions. Pourtant le principe sous-jacent ne dépend d’aucun moteur en particulier : il tient en deux idées et quatre réflexes opérationnels, applicables que vous tourniez sur GitHub Actions, GitLab CI, Jenkins, Azure DevOps ou CircleCI
Note du PandaRedacteur: ou sur PandaCI ….
Les deux idées d’abord. Séparer ce qui est non privilégié de ce qui l’est : un job qui checkoute et exécute du code d’un contributeur externe ne doit jamais, dans le même contexte d’exécution, avoir accès aux secrets de déploiement.
-
Idee 1 : Le build et les tests d’une contribution externe tournent dans un job sans secret ; la publication, la signature ou le déploiement tournent dans un job séparé, déclenché seulement après revue.
-
Idée 2 : Et remplacer le secret stocké par une identité éphémère : un jeton OIDC vers le cloud provider ou le registry est lié à l’identité du job et expire à la fin de son exécution, contrairement à un secret longue durée que le code de l’attaquant pourrait exfiltrer une fois pour toutes.
Les quatre réflexes :
- Permissions par défaut en lecture seule. Le token par défaut de n’importe quel job de CI doit être en lecture seule. L’élévation à l’écriture doit être explicite, scopée au strict nécessaire, et déclarée job par job, jamais au niveau global du pipeline.
- Approbation humaine avant exécution privilégiée. Toute exécution de code externe dans un contexte privilégié doit passer par une étape de revue explicite et bloquante, pas par une configuration automatique.
- Runners éphémères. Un agent qui survit à plusieurs jobs est une cible de persistance. Il doit naître pour un job et disparaître ensuite.
Note du PandaRedacteur : Attention aux caches….
- Auditer les définitions de pipeline elles-mêmes. Un fichier de configuration CI est du code. Il mérite un lint de sécurité dédié, pas seulement une revue humaine rapide.
Le vocabulaire change d’un moteur à l’autre. Le risque, non.
DevSecOps : intégrer ce contrôle quel que soit le pipeline
Ce contenu représente de nombreuses heures de travail, d'expérience etc... J'ai remarqué que mon contenu était repris par certaines sociétés/personnes et j'ai donc décidé de donner du contenu minimal sur ce blog. C'est pourquoi je vous invite a me contacter sur LinkedIn en mentionnant cet article pour plus d'informations.