Reconstitution de la compromission de l’action GitHub Changed Files de TJ Actions
17 mars 2025
0 minutes de lectureDans l’après-midi du vendredi 14 mars 2025, des détails ont commencé à émerger sur une grave faille de sécurité touchant une action GitHub populaire appelée changed files (tj-actions/changed-files). Environ 23 000 dépôts GitHub utilisent cette action dans leurs workflows CI et DevOps. Elle permet de suivre les fichiers modifiés entre différentes branches et différents commits.
Un attaquant disposant de droits d’écriture sur le dépôt de l’action a créé un commit qui a fait apparaître des secrets chiffrés en clair dans les journaux GitHub Actions. Cela pouvait entraîner une grave compromission d’un dépôt public, dont les journaux d’action sont eux aussi publics.
Tout d’abord, nous tenons à rassurer nos clients : Snyk a terminé l’évaluation de son infrastructure et nous pouvons confirmer que nous n’utilisons aucune version vulnérable concernée de cette bibliothèque. Snyk est donc protégé contre cette vulnérabilité.
Cela dit, nous souhaitons vous présenter l’analyse approfondie de cette vulnérabilité réalisée par Snyk, expliquer son fonctionnement et proposer des mesures correctives concrètes. C’est l’objet de cet article.
Au moment de la rédaction de cet article, on ignore pourquoi l’attaquant disposait de droits d’écriture sur le dépôt qui héberge le code de cette action GitHub. Au-delà de ces droits, les principaux facteurs ayant rendu l’attaque possible étaient les suivants :
Modifier des tags de version existants pour les faire pointer vers le commit malveillant
Isoler le commit malveillant de toutes les branches, y compris la branche principale
Ces facteurs ont ajouté une couche de dissimulation pour masquer l’attaque. Celle-ci reposait sur un appel réseau externe pour récupérer le code malveillant, ce qui a fini par attirer l’attention de services surveillant les activités réseau anormales.
Vers la fin de cet article, je reproduis l’attaque (sans danger) pour vous aider à comprendre comment elle a été possible et comment vous en protéger.
À propos des commits Git orphelins
Voici un exercice à essayer. Créez un nouveau dépôt sur GitHub et clonez-le localement. Créez une branche et poussez-la sur GitHub. Notez le hash du commit. Supprimez ensuite la branche distante. Vous pouvez toujours accéder à ce commit, même s’il n’est plus associé à aucune branche existante.
Voici les étapes décrites ci-dessus :
1. Créez un dépôt sur GitHub
2. Créez un dépôt local et ajoutez-y le dépôt distant GitHub que vous venez de créer
3. Créez une branche
4. Ajoutez un commit et poussez-le sur GitHub
5. Notez le commit sur GitHub

6. Supprimez la branche sur GitHub

7. Accédez à l’URL du commit que vous avez notée précédemment

Voici à quoi ressemble une branche orpheline. C’est l’un des principaux facteurs ayant permis l’exploitation du workflow GitHub Action changes-files.
L’autre facteur clé a été le déplacement des tags de version dans le dépôt.
À propos des tags de version GitHub
Malheureusement, les développeurs prêtent parfois aux tags de version GitHub des pouvoirs qu’ils n’ont pas. Si l’on nous dit d’utiliser la version v35 d’une version donnée, nous faisons référence à ce tag et supposons que nous obtiendrons bien cette version. Dans des conditions normales, c’est une hypothèse raisonnable, mais les tags GitHub ne sont que des chaînes pratiques qui pointent vers un commit précis : il n’y a rien de magique, même avec une gestion rigoureuse du versionnage sémantique. Voici un extrait de fichier YAML d’une action GitHub qui fait référence à l’action changes-files :
L’élément crucial se trouve à la toute dernière ligne, qui fait référence à un tag particulier du dépôt de l’action GitHub. L’attaquant a supprimé les tags de version d’origine et les a réaffectés au commit malveillant, qui était orphelin. Il a redirigé plusieurs tags de version existants vers son commit malveillant, amplifiant ainsi l’impact de l’attaque.
Je peux le démontrer en poursuivant l’exemple précédent. Sur ma machine locale, je vais créer un tag pour la branche sur laquelle je travaille :
Sur GitHub, un tag est désormais associé au commit orphelin. Je peux accéder à :
Je vois alors ceci :

L’attaquant pouvait donc faire pointer les utilisateurs vers le commit malveillant en mettant simplement à jour le dépôt du code de l’action GitHub changed-files.
En résumé, un acteur malveillant avait accès en écriture à un dépôt dont dépendaient 23 000 autres dépôts. L’attaquant a imaginé une méthode astucieuse pour dissimuler son activité, mais sans cet accès en écriture, l’attaque n’aurait pas été possible.
Examinons de plus près ce que cherchait l’attaquant.
À propos de la fuite de secrets
Les secrets sont souvent nécessaires pour que les scripts puissent interagir avec d’autres services ou pour fournir les clés indispensables à la compilation et à l’exécution des tests.
GitHub dispose d’un système de chiffrement sophistiqué pour gérer les secrets. Vous pouvez définir un secret au niveau du dépôt Git et y accéder dans votre script de compilation sans qu’il apparaisse dans les journaux de compilation ni qu’il soit divulgué. En arrière-plan, GitHub déchiffre le secret et l’utilise comme référence dans votre script de compilation. En pratique, votre fichier YAML GitHub Actions peut contenir une ligne semblable à celle-ci :
Le reste du script peut alors accéder au secret sans qu’il soit nécessaire de l’inclure dans le script ou les journaux.
L’attaquant a modifié l’action GitHub changed-files de TJ pour télécharger un script depuis un emplacement distant, l’enregistrer dans un fichier local sur la machine virtuelle exécutant l’action GitHub, puis l’exécuter. L’opération s’est déroulée en deux étapes. La première a consisté à créer le commit malveillant orphelin, qui a depuis été supprimé. Voici la partie essentielle de ce commit Git :
Si vous décodez en Base64 la longue chaîne ci-dessus (comme le fait le script), vous obtenez :
Le gist auquel il est fait référence a depuis été supprimé, mais voici le script Python qu’il contenait :
Le script ci-dessus, exécuté en tant que superutilisateur à l’aide de la commande sudo, analyse la mémoire des processus pour trouver les secrets déchiffrés et les afficher dans le journal Actions. C’est particulièrement dommageable, car les journaux GitHub Actions des dépôts publics sur GitHub sont eux aussi publics par conception.
Une publication sur Hacker News (disponible ici) parue le 14 mars indiquait que l’exploit avait été détecté par StepSecurity, un service qui surveille notamment les appels réseau non autorisés dans GitHub Actions. L’auteur et responsable de la maintenance de cette action GitHub populaire a ajouté un commentaire à cette publication pour expliquer ce qui s’était passé. Le tout premier problème qu’il relève est que l’attaquant avait accès en écriture au dépôt.
Grâce à la détection rapide de l’exploit et à la réactivité du responsable de la maintenance, la vulnérabilité a été corrigée et l’attaque stoppée très rapidement. Toutefois, les utilisateurs de l’action GitHub changed-files sont invités à examiner leurs journaux depuis le 14 mars pour vérifier qu’aucun secret n’a été divulgué dans un journal Actions public.
L’exploit en action
J’ai créé un ensemble de dépôts pour reproduire cet exploit avec une configuration aussi proche que possible de celle de l’attaquant.
Le premier dépôt Git contient la définition de l’action GitHub personnalisée : tj-changed-files-action-goof.
Le fichier index.js est simple et reprend principalement le tutoriel GitHub sur la création d’actions :
Il attend une entrée appelée who-to-greet et renvoie la date et l’heure en sortie.
L’autre dépôt ne contient qu’un seul fichier : un fichier YAML configuré pour exécuter l’action GitHub personnalisée. Il contient aussi un secret nommé A_SECRET. Le dépôt s’appelle : tj-changed-files-action-exploit-goof. Si vous consultez son fichier .github/workflows/main.yml, vous verrez ceci :
Il effectue plusieurs opérations :
Afficher un message « Hello, world! »
Exécuter l’action GitHub
tj-changed-files-action-goofAfficher l’heure renvoyée par l’action. Le secret du dépôt est également utilisé de la manière prévue dans une action GitHub.
Comparez le résultat de A Goof Step dans deux exécutions différentes. D’abord, celle-ci :
Ensuite, celle-ci :
Dans les deux cas, snyk-labs/tj-changed-files-action-goof@v1.0 est exécutée. Dans la deuxième exécution, vous pouvez voir le résultat de l’action GitHub compromise. Si nous décodons deux fois la chaîne encodée en Base64, nous obtenons :
Dans les deux exécutions, vous pouvez constater que GitHub Actions protège la valeur du secret lorsqu’elle est référencée : a_secret: ***. Toutefois, le code de l’exploit extrait directement de la mémoire système les valeurs des secrets en clair.
Si nous revenons au tag v1.0 du dépôt tj-changed-files-action-goof, vous verrez une branche orpheline semblable à celle de l’exploit d’origine :

Au départ, v1.0 pointait vers la branche main. Mais pour simuler l’exploit, je l’ai simplement fait pointer vers une branche attack, que j’ai ensuite rendue orpheline :
Le dernier détail piégeux, facile à manquer, est ncc build index.js. Cette commande met à jour dist/index.js, et seul ce fichier mis à jour est commité. C’est le fichier dist/index.js regroupé avec webpack que l’action GitHub exécute réellement. Ainsi, si vous consultez le fichier index.js de la branche v1.0 (orpheline), rien ne semble avoir changé.
Voici le diff du code webpack dist/index.js sur v1.0, comparé à la branche main.
Protégez vos workflows GitHub Actions
Je suis certain que nous en apprendrons davantage sur les raisons pour lesquelles l’attaquant disposait de droits d’écriture sur le dépôt de cette action GitHub populaire.
Il est courant d’utiliser les tags Git comme références de version pour les différentes bibliothèques dont nous dépendons.
Il existe plusieurs moyens d’éviter cet exploit en particulier.
1. Pour les actions GitHub personnalisées distantes, référencez directement le hash du commit
Par exemple, vous pourriez :
Cette méthode référence directement le hash du commit, plutôt qu’un tag. Ce n’est pas une pratique courante, mais elle garantit que la version exécutée de l’action GitHub personnalisée est bien celle que vous attendez.
2. Utilisez d’autres actions GitHub personnalisées pour détecter les appels réseau inattendus. C’est ainsi que StepSecurity a repéré l’exploit.
Il est encourageant de voir une communauté de contributeurs open source, d’entreprises et de particuliers se mobiliser pour détecter et corriger les nouveaux exploits dès leur apparition, comme celui-ci.
Snyk a publié des recherches et des bonnes pratiques sur GitHub Actions. Nous vous encourageons vivement à les consulter et à évaluer vos pratiques DevSecOps et de sécurité à l’aide des ressources suivantes :
Appel à l’action : explorer les vulnérabilités de GitHub Actions
Créer un pipeline CI/CD sécurisé avec GitHub Actions pour votre application Java
Publier des packages npm en toute sécurité avec GitHub Actions
Découvrez l’état de la sécurité des logiciels open source
Découvrez les tendances et les approches actuelles en matière de logiciels open source et de sécurité de la chaîne d’approvisionnement.
