· Security musings

Catégories

Tags

🔍 Licence d'Utilisation 🔍

Sauf mention contraire, le contenu de ce blog est sous licence CC BY-NC-ND 4.0.

© 2025 à 2042 Sébastien Gioria. Tous droits réservés.

⏱️
Temps de lecture estimé
~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 :

  1. Le mainteneur configure un workflow pull_request_target pour pouvoir poster un commentaire avec les résultats de couverture sur les PR externes, ce qui nécessite le GITHUB_TOKEN en écriture.
  2. Pour calculer cette couverture, le workflow fait un actions/checkout avec ref: $, donc le code exact proposé par le contributeur.
  3. L’attaquant ouvre une PR depuis un fork. Son code modifie un fichier de configuration de test (jest.config.js, pytest.ini, un hook postinstall de package.json) pour exécuter une commande arbitraire au moment du build.
  4. Le workflow s’exécute automatiquement, dans le contexte du dépôt cible, avec le GITHUB_TOKEN en écriture et les secrets éventuellement exposés à ce job.
  5. 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.
  6. 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.
Moteur CI Mécanisme à risque Contrôle équivalent
GitHub Actions pull_request_target + checkout de la PR, workflow_run + artefact non vérifié actions/checkout@v7, permissions: read-all par défaut, job de déploiement séparé avec environment et reviewers obligatoires
GitLab CI Merge request pipelines avec variables protégées exposées à une MR de fork rules excluant les forks pour les jobs sensibles, variables marquées protected, séparation build / deploy en stages distincts
Jenkins Multibranch Pipeline ou PR Builder Plugin exécutant le Jenkinsfile du fork avec les credentials globaux Credentials scopés par job (withCredentials ciblé), étape input d'approbation avant les stages privilégiés, agents éphémères
Azure DevOps Build validation policy sur PR de fork avec accès aux variables du pipeline Paramètre "Make secrets available to builds of forks", "Pause pipeline until permission granted"
CircleCI Forked pull request build avec accès aux contextes du projet Désactiver "Pass secrets to builds from forked pull requests", utiliser des contextes scopés par environnement

Le vocabulaire change d’un moteur à l’autre. Le risque, non.

DevSecOps : intégrer ce contrôle quel que soit le pipeline

Phase Outil Action
Code zizmor, lint CI dédié Détecter les triggers privilégiés combinés à un checkout non vérifié avant même le merge.
Build Runners éphémères, permissions read-only Exécuter le code des contributions externes sans aucun secret accessible.
Deploy OIDC, environnements protégés Réserver l'identité privilégiée à un job séparé, déclenché après approbation humaine.
Monitor Audit logs du moteur CI Alerter sur toute exécution d'un job privilégié déclenchée par une PR ou MR externe.
🔒 Plus d'éléments en contenu premium
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.

Recommandations de mitigation github-actions

Mettre à jour vers actions/checkout

....

Linter les définitions de pipeline

....

Recommendations de mitigation gitlab-ci

.........

Recommendations de mitigation Jenkins

....
À retenir 📌

  • Le pwn request n'est pas une faille GitHub : c'est un motif générique qui existe partout où un trigger privilégié peut checkouter du code non vérifié.
  • Mettre à jour vers actions/checkout v7 : la protection contre le checkout de PR de fork est désormais activée par défaut.
  • Séparer systématiquement build non privilégié et déploiement privilégié, quel que soit le moteur CI utilisé.
  • Permissions read-only par défaut, OIDC plutôt que secrets longue durée.
  • GitLab CI et Jenkins ont les mêmes risques sous d'autres noms : merge request pipelines, Multibranch Pipeline, PR Builder Plugin.
  • Auditer les fichiers de pipeline comme du code : zizmor pour GitHub Actions, équivalents pour les autres moteurs.