Le 14 mars 2025, l’action GitHub tj-actions/changed-files a été compromise : un tag réécrit a pointé vers du code qui exfiltrait les secrets des runners vers les logs publics. Environ 23 000 dépôts l’utilisaient. Pas besoin d’exploiter une faille applicative — il suffisait de contrôler un tag Git pour réécrire silencieusement toutes les références qui pointaient vers lui.
Dix-huit mois plus tard, GitHub a généralisé une réponse structurelle à ce type d’attaque : les immutable actions. Le principe est simple à énoncer, un peu moins simple à mettre en œuvre correctement — et surtout, ça ne protège personne tant que tu n’as pas changé quelque chose dans tes workflows.
Ce qui s’est cassé avec tj-actions/changed-files
Référencer une action par un tag comme @v45 revient à faire confiance à ce que ce tag désigne au moment de l’exécution, pas au moment où tu as écrit ton workflow. Un tag Git n’est qu’un pointeur : son propriétaire peut le faire pointer ailleurs sans que personne ne le remarque. C’est exactement ce qui s’est produit avec tj-actions/changed-files (CVE-2025-30066) : les tags de version ont été réécrits pour exécuter un script qui dumpait la mémoire des runners dans les logs publics, à la recherche de secrets.
Le pinning par SHA de commit complet (@a1b2c3d...) protège contre ce scénario précis. Mais il a une limite que peu de gens vérifient : une action composite peut elle-même appeler d’autres actions, et ces dépendances-là restent mutables même si l’action de premier niveau est épinglée. Tu peux donc être rigoureux sur ton propre workflow et rester exposé via une couche que tu ne vois jamais.
Comment fonctionnent les immutable actions
Depuis leur généralisation (suivie publiquement sur la roadmap GitHub), une action publiée en version immuable n’est plus distribuée comme un simple tag Git : elle devient un artefact OCI, stocké dans GitHub Packages et adressé par un digest de contenu. Ce digest ne change jamais pour une version donnée — toute correction passe obligatoirement par une nouvelle version.
Concrètement, à l’exécution d’un job, le runner résout la version demandée, récupère le digest attendu, et le compare à celui qu’il vient de télécharger avant d’exécuter le moindre code de l’action. Un écart interrompt le job. Les actions composites sont dépliées récursivement, donc le graphe complet des dépendances — y compris celles que tu ne déclares jamais toi-même — devient vérifiable de la même façon.
Et le pinning par SHA, alors ?
Il reste utile, notamment pour les actions qui n’ont pas encore migré vers la distribution immuable — ce qui concerne la grande majorité des dizaines de milliers d’actions publiées sur le Marketplace. On avait détaillé les bonnes pratiques de pinning juste après l’incident tj-actions, dans cet article : elles restent valables, les deux mécanismes se complètent plutôt qu’ils ne se remplacent.
Publier une action immuable (côté mainteneur)
Si tu maintiens une action, le passage se fait via le workflow dédié actions/publish-immutable-action, qui prend ton code existant et le publie dans GitHub Packages sous forme d’image versionnée, selon un modèle build → package → publish entièrement automatisé. Côté utilisateurs de ton action, rien ne change dans leur syntaxe : une référence par tag (@v3) continue de fonctionner, elle pointe simplement désormais vers une version packagée et vérifiée plutôt que vers un tag Git classique. Seules les références par SHA de commit brut ou par nom de branche doivent migrer vers une référence de version.
Ce que la généralisation ne fait pas automatiquement
C’est le point que la plupart des annonces passent sous silence : la disponibilité de la fonctionnalité n’oblige personne à l’utiliser, et GitHub ne propose aujourd’hui aucune politique native pour l’exiger. Tu peux avoir des centaines de workflows qui référencent encore des actions mutables sans qu’aucun signal ne te le dise — sauf à le vérifier toi-même.
Une solution communautaire répandue consiste à transformer cette vérification en required workflow, via l’action joshjohanning/ensure-immutable-actions (décrite en détail ici). Elle classe chaque action référencée dans un workflow en trois catégories — première partie (actions/*, github/*), immuable (release immuable ou SHA complet), ou mutable — et fait échouer la vérification si une action mutable est détectée :
name: Check Action Immutability
on:
pull_request:
permissions:
contents: read
jobs:
check-immutable:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Ensure immutable actions
uses: joshjohanning/ensure-immutable-actions@v2
Ce workflow se déclare ensuite comme obligatoire dans un ruleset d’organisation (Settings → Rules → Rulesets → Require workflows to pass before merging), en mode actif pour bloquer les PR non conformes, ou en mode évaluation pour observer avant de durcir la règle. L’intérêt de ce contrôle continu : même si un mainteneur désactive l’immuabilité sur sa prochaine release, la PR suivante qui référence cette version mutable se fait signaler.
Ce que ça ne règle pas
L’immuabilité verrouille une version publiée ; elle ne dit rien de ce qu’un mainteneur légitime décide de publier ensuite. Si son compte est compromis, ou s’il publie délibérément une nouvelle version piégée, cette nouvelle version sera immuable elle aussi — et tout aussi dangereuse. La revue de code et le scan de dépendances restent nécessaires, l’immuabilité ne les remplace pas.
Deux autres limites à connaître : la signature et l’attestation de provenance (via Sigstore), ainsi que le scan anti-malware des actions publiées, sont présentées comme des évolutions à venir, pas comme des protections déjà actives aujourd’hui. Et une grande partie des actions les moins utilisées du Marketplace ne migreront probablement jamais, faute de maintenance active — ta politique de blocage doit rester la solution de repli pour ces cas-là, pas l’immuabilité seule.
FAQ
Les immutable actions remplacent-elles le pinning par SHA ?
Non. Elles couvrent un angle mort du pinning par SHA (les dépendances internes des actions composites), mais tant qu’une action n’a pas migré, le SHA reste ta seule protection contre la réécriture de tag.
Dois-je changer la syntaxe de mes workflows existants ?
Pas si tu références déjà les actions par tag de version (@v4, par exemple). Seules les références par SHA de commit brut ou par branche nécessitent une mise à jour vers une version sémantique.
Comment savoir si une action que j’utilise est déjà immuable ?
Aucune commande native ne le liste encore de façon exhaustive ; des outils tiers comme le workflow ensure-immutable-actions ci-dessus font ce travail de vérification à ta place.
Passe à l’action
Commence par vérifier si les actions tierces les plus critiques de ton organisation — celles qui touchent au déploiement ou à la publication d’artefacts — ont déjà une version immuable disponible. Si oui, mets à jour tes références de version. Si non, le pinning par SHA reste ta meilleure protection en attendant. Et dans tous les cas, un required workflow qui vérifie ça automatiquement t’évite de devoir y repenser à chaque nouveau pipeline.

Laisser un commentaire