Le secret que le masque a raté : lire une fuite CI en double base64
Le défi
Une action CI piratée a écrit ceci dans son log. Retire le base64 qui a esquivé le masquage. Récupère le jeton de publication npm volé.
Ce que tu vas apprendre
- Expliquer comment la compromission de tj-actions/changed-files a fait fuiter des secrets CI
- Reconnaître une sortie décodée qui est elle-même du base64
- Expliquer pourquoi une deuxième couche d'encodage contourne le masquage des logs de GitHub
- Distinguer un jeton de job à courte durée de vie d'un jeton de publication à longue durée de vie
Compétences testées
Prérequis
- Ce qu'est le base64
- Ce que fait un pipeline CI
Comment ça marche
GitHub Actions masque les secrets dans les logs de job : avant qu'une ligne soit écrite, le runner remplace chaque valeur de secret enregistrée par ***. Il masque aussi les formes base64 de la valeur, aux trois décalages d'octets possibles, parce que echo $TOKEN | base64 est le plus vieux truc du métier.
En mars 2025, un attaquant a redirigé tous les tags de tj-actions/changed-files, utilisée par environ 23 000 dépôts, vers un commit malveillant. Le nouveau code lisait la mémoire du processus du runner, filtrait avec grep chaque entrée "name":{"value":"...","isSecret":true} et les affichait avec base64 -w 0 | base64 -w 0. La deuxième couche change chaque caractère, donc rien dans la ligne ne correspond à un motif masqué. Sur les dépôts publics, n'importe qui pouvait ouvrir l'exécution et lire les secrets. Un log du dépôt fictif de ce hack ressemblait à ceci :
##[group]Run ridgeway-actions/changed-paths@v4 with: separator: , files: src/** env: NPM_TOKEN: *** ##[endgroup] SWtOUFJFVkRUMVpmVkU5TFJVNGlPbnNpZG1Gc2RX...
Le bloc env: montre le masque qui fonctionne sur la valeur en clair. La dernière ligne est le même secret, encodé deux fois, et le masque ne l'a jamais vu.
Erreurs fréquentes
- S'arrêter après un seul décodage. Le premier résultat ne contient encore que des lettres, des chiffres,
+,/et=: c'est du base64 à nouveau. Décode jusqu'à obtenir du texte lisible. - Répondre avec le jeton
ghs_. Il apparaît deux fois (github_tokenetsystem.github.token) parce que c'est leGITHUB_TOKENdu job lui-même. GitHub le génère pour chaque job et il expire à la fin du job. L'objectif est le jeton de publication npm, qui continue de fonctionner après l'exécution. - Répondre avec la valeur Codecov. L'UUID appartient à
CODECOV_TOKEN, un jeton d'upload, pas celui de npm. - Appuyer une troisième fois sur base64. Le JSON n'est pas du base64, donc l'étape échoue : cet échec t'indique que tu as déjà atteint le texte en clair.
Comment s'en protéger
- Épingle les actions tierces sur un SHA de commit complet de 40 caractères, pas sur un tag : les tags peuvent être déplacés, c'est exactement ce qui est arrivé à tj-actions.
- Publie sur npm avec la publication de confiance (OIDC), pour qu'aucun jeton npm à longue durée de vie ne traîne dans tes secrets.
- Ne donne un secret qu'au job ou à l'environnement qui en a besoin : un job qui liste les fichiers modifiés n'a aucune raison de voir
NPM_TOKEN. - Surveille le trafic sortant des runners (par exemple avec StepSecurity Harden-Runner, qui a repéré le téléchargement du payload tj-actions).
- Après un incident, change chaque secret que les exécutions concernées pouvaient lire et supprime les logs publics.