Le secret que le masque a raté : lire une fuite CI en double base64

Investigation Numérique & Réponse à Incident Niveau 2/4 ~3 min 14 novembre 2026

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

Reconnaissance d'encodagesRéponse à incident CI/CDTri des secrets

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_token et system.github.token) parce que c'est le GITHUB_TOKEN du 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.

Solution complète

Les membres Pro et Max débloquent la solution complète étape par étape.

Passer Pro

Statistiques de la communauté

185 résolutions
91% taux de réussite
M2F14M3 Premier sang

Hacks du jour associés

38 000+ Hackers Vrais labs Gratuit
Commencer Gratuitement ou résolvez le hack du jour, sans compte