DevSecOps

Cache poisoning des GitHub Actions : le vecteur Shai-Hulud à ajouter à votre threat model

Le worm Shai-Hulud a empoisonné le cache GitHub Actions pour injecter du code malicieux dans des builds CI/CD légitimes. Mécanique et défense.

Aroua Biri

La partie la plus inquiétante du worm Shai-Hulud n'est ni le payload ni le vol de tokens. C'est le cache poisoning du runner GitHub Actions, démontré sur le dépôt TanStack. La mécanique est simple, élégante côté attaquant, et invisible côté défense classique. Elle mérite d'entrer dans tout threat model CI/CD à partir de 2026.

Le scénario tel qu'il s'est passé

  1. L'attaquant fork publiquement le dépôt TanStack/router (action légale, accessible à n'importe qui).
  2. Il ouvre une pull request depuis ce fork vers le dépôt original.
  3. Le workflow pull_request_target du dépôt original se déclenche avec les permissions du dépôt cible, pas du fork.
  4. Le job utilise pnpm. Au démarrage, il restaure le store pnpm depuis le cache GitHub Actions.
  5. L'attaquant a empoisonné ce cache lors d'une exécution précédente, en y plaçant un binaire pnpm modifié.
  6. Le binaire empoisonné s'exécute au moment du pnpm install dans le workflow légitime, avec les secrets et permissions du dépôt cible.
  7. Le payload exfiltre les tokens et publie des versions malicieuses depuis le compte de maintenance.

Trois CVE GitHub Actions chaînées, un cache restauré sans vérification d'intégrité, et voilà.

Le malentendu qui rend l'attaque possible

Beaucoup de devs pensent que le cache GitHub Actions est isolé par branche ou par dépôt. Ce n'est pas exact. Le cache est partagé entre les workflows d'un même dépôt, et accessible en écriture par un fork qui déclenche un workflow via pull_request_target.

Côté GitHub, c'est documenté. Côté équipes prod, c'est largement méconnu.

Quatre actions à mener dans la semaine

1. Auditer les workflows en pull_request_target

``bash grep -r "pull_request_target" .github/workflows/ ``

Pour chaque match, deux questions :

  • Le workflow lit-il du contenu du fork (notamment via actions/checkout avec ref: ${{ github.event.pull_request.head.sha }}) ?
  • Le workflow utilise-t-il un secret ou un token avec scope élargi ?

Si oui aux deux, c'est une exposition immédiate.

2. Désactiver le restore de cache pour les builds liés à des forks

Tant que GitHub Actions ne propose pas un contrôle granulaire (en discussion, pas livré au 21 mai 2026), la position prudente est :

  • Pas de actions/cache@v4 dans un job déclenché par pull_request_target.
  • Ou activation conditionnelle uniquement quand github.event.pull_request.head.repo.full_name == github.repository (auteur interne, pas fork).

3. Pinner les actions par SHA, pas par tag

Une action utilisée comme actions/checkout@v4 peut voir son tag déplacé vers un commit malicieux. La forme sûre :

``yaml uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 ``

Verbeux, oui. Mais c'est aussi la seule manière de garantir qu'un commit attendu est exécuté. Pinner par SHA + commentaire de version est devenu le baseline 2026, et c'est aussi ce qu'attend la majorité des audits SOC 2 / ISO 27001 modernes.

4. OIDC pour publier vers npm, pypi, Docker Hub

Remplacer les NPM_TOKEN long-lived par une publication OIDC depuis le runner. Le token est généré à la volée, valable quelques minutes, scopé à un package précis. Si un attaquant l'intercepte, il n'a presque rien.

Le sujet de fond : la frontière de confiance

Le cache poisoning Shai-Hulud illustre un problème plus large : la frontière de confiance dans CI/CD est mal définie. Les ingénieurs raisonnent souvent en "mon code" vs "code externe". La réalité est plus nuancée :

  • Le code source est sous votre contrôle (review, signature).
  • Les dépendances sont sous contrôle des mainteneurs (faible, on l'a vu).
  • Les actions GitHub sont du code tiers exécuté avec accès à tout.
  • Le cache est un état persistant entre exécutions, modifiable par des contributeurs externes dans certains modes.
  • Le runner lui-même peut être un runner self-hosted partagé entre équipes.

Chacun de ces niveaux a sa propre surface d'attaque. Les guides ANSSI, NIST SP 800-204D et le CIS Software Supply Chain Security Benchmark commencent à les structurer, mais peu d'équipes ont fait l'exercice formellement.

Le contre-exemple : ce qui n'aurait pas marché

Si TanStack avait :

  • Pinning des actions par SHA,
  • Pas de cache restore en pull_request_target,
  • OIDC pour la publication npm,

…le worm n'aurait pas eu de chemin. Ces trois mesures ne coûtent qu'une demi-journée par dépôt critique. Beaucoup ne sont pas faites, parce qu'elles ne sont pas dans le workflow par défaut quand on initialise un projet.

Pour la lecture du worm Shai-Hulud lui-même, Shai-Hulud : 314 packages npm en 12 jours. Pour la vuln pull_request_target, pull_request_target : ce que 60% des repos publics ont encore.

Un sujet connexe chez vous ?

20 minutes pour cadrer ensemble. Aucune offre commerciale envoyée à froid.

Réserver un échange Calendly