Issue2Shell: o título de uma issue virou comando executado no GitHub Actions — anatomia e 3 blindagens
Em 1º de setembro, uma automação despejou 83 issues, pull requests e comentários maliciosos em 61 repositórios públicos do GitHub — 77 deles em cerca de 43 minutos. Entre os alvos: projetos da Google (LangExtract), o Trivy, o Brave, MLflow, ToolJet e PX4-Autopilot.
O vetor não foi malware, nem credencial vazada. Foi o título da issue.
Batizei esse padrão de Issue2Shell, porque ameaça com nome a gente não esquece. Vou destrinchar o mecanismo e o que fazer no seu pipeline — porque o truque é quase constrangedor de tão simples, e eu apostaria que existe workflow vulnerável a isso em muita esteira por aí.
O mecanismo
Muitos workflows do GitHub Actions disparam sozinhos quando alguém abre uma issue ou PR: checar o título, aplicar label, mandar notificação. Coisa inofensiva — até o workflow interpolar texto de estranho direto num script shell:
# VULNERÁVEL — não faça isso
on:
issues:
types: [opened]
jobs:
check:
runs-on: ubuntu-latest
steps:
- run: echo "Nova issue: ${{ github.event.issue.title }}"
A expressão ${{ }} é expandida antes de o Bash rodar. Se o título contiver uma substituição de comando — um $( ) —, aquilo deixa de ser texto e vira comando executado no runner. E não: aspas duplas não salvam, porque a expansão acontece dentro delas.
No caso real, o comando injetado coletava as variáveis de ambiente do runner (onde costumam morar tokens e credenciais), codificava e mandava para fora via HTTP.
O que foi confirmado — e o que não foi
Aqui a parte que as manchetes arredondaram, e que vale registrar com precisão:
- ✅ Execução dos comandos injetados foi confirmada em 2 workflows (PX4-Autopilot e workout-cool) — não nos 61 repositórios. 61 foram mirados, não invadidos.
- ❌ Não há confirmação de roubo de secrets, acesso de escrita ou release comprometido.
- ⚠️ Secrets do Actions não ficam disponíveis a qualquer workflow — execução ≠ vazamento.
- ⚠️ Há sinais de que parte das contas que postaram as issues eram contas sequestradas de usuários legítimos.
E é exatamente essa incerteza que incomoda: quando variável de ambiente sai do runner, ninguém sabe dizer com certeza o que saiu junto.
As 3 blindagens
1. Texto de fora nunca entra direto no shell. Passe por variável de ambiente — o shell recebe o valor como dado, não como código:
# CORRIGIDO
- env:
TITULO: ${{ github.event.issue.title }}
run: echo "Nova issue: $TITULO"
2. Permissão mínima no workflow. permissions: read-all (ou menos) por padrão no topo do workflow; escrita só no job que realmente precisa. O GITHUB_TOKEN com escopo largo transforma qualquer injeção em incidente grande.
3. Separe o job que lê conteúdo público do workflow que tem poder. Notificação/triagem de issue não deveria viver no mesmo workflow que faz release, publica pacote ou tem acesso a secrets de produção.
E se você encontrar evidência de execução num workflow seu: trate como incidente — revise o que aquele job enxergava e rotacione as credenciais. Execução sem prova de exfiltração ainda exige rotação; a dúvida joga contra você.
A lição maior
CI/CD não é bastidor. Pipeline com segredos que reage automaticamente a texto de estranho é superfície de ataque pública — a mesma tese dos ataques de supply chain no front-end, só que do lado da esteira.
Pergunta honesta para quem mantém pipeline: quantos dos seus workflows processam título ou corpo de issue/PR hoje — e quem olhou esses arquivos pela última vez? Se a resposta for "não sei", o grep abaixo é um bom começo:
grep -rn "github.event" .github/workflows/ | grep -E "title|body"
Escrevo a série segurança sem hype — o golpe do momento explicado antes de você cair nele. Fontes do caso: relatório da Radware (out/2026) e cobertura do Estadão/TecMundo.