1

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.

Carregando publicação patrocinada...