O gatilho que mudou tudo: lendo o diff de um workflow em busca de segredos expostos
O desafio
O CI da Harbourline funcionou sem problemas por meses. Aí um mantenedor notou que o comentário de cobertura nunca aparecia em pull requests vindas de forks e corrigiu isso em uma linha, na revisão 13. O pipeline continua compilando, testando, comentando, e agora faz tudo isso para qualquer pessoa na internet que abra uma pull request. Três revisões do workflow estão aqui, mais o workflow de release e o package.json como contexto. Leia o que a revisão 13 mudou, descubra qual passo executa código escrito pelo contribuidor, e diga qual segredo esse passo entrega a ele.
O que você vai aprender
- Explicar a diferença entre pull_request e pull_request_target
- Rastrear qual job e qual step executa código controlado pelo contribuidor
- Determinar quais segredos são acessíveis a partir de um step específico
- Reconhecer scripts de lifecycle do npm como um sink de execução no CI
- Usar os limites entre jobs e as condições para incluir ou descartar segredos na análise
Habilidades testadas
Pré-requisitos
- Leitura de YAML
- O que um pipeline de CI faz em uma pull request
Como funciona
Um sistema de CI que compila pull requests precisa responder uma pergunta antes de qualquer outra coisa: de quem é esse código, e o que ele pode alcançar. O pull_request responde de forma conservadora. O job roda no contexto do fork, os segredos ficam de fora, e o token automático é somente leitura, então o código de um estranho executa mas não encontra nada que valha a pena roubar.
O pull_request_target responde de outra forma. O job roda no contexto do repositório base, com todo o cofre de segredos e um token com permissão de escrita, que é a única forma de um workflow etiquetar, comentar ou triar uma contribuição externa. A segurança dessa troca depende inteiramente de uma suposição: o job nunca executa nada que o contribuidor escreveu. Por padrão, ele faz checkout do branch base justamente para manter isso verdadeiro.
No momento em que alguém adiciona ref: github.event.pull_request.head.sha para fazer o build realmente testar a mudança, essa suposição deixa de existir e as duas metades se combinam em execução remota de código com credenciais de produção. Em um projeto Node não existe nem um passo de build para subverter: o npm ci executa os lifecycle scripts do próprio package.json do contribuidor antes de qualquer um dos seus comandos.
Erros comuns
- Responder GITHUB_TOKEN. Ele é elevado sob esse gatilho, mas é referenciado no job
comment, que nunca faz checkout do código do contribuidor. - Responder CODECOV_TOKEN. Mesmo job do passo de comentário, mesmo motivo.
- Responder SLACK_WEBHOOK. Seu passo é protegido por
github.event_name == 'push', e essa execução é um evento de pull request. - Responder GPG_SIGNING_KEY. Isso está em
release.yml, que só é disparado por um push de tag. - Culpar a r12. A r12 já reutilizava o token de publicação, o que é descuidado, mas sob
pull_requestnenhum fork conseguiria vê-lo. A exposição começa na r13.
Como se proteger
Mantenha o contexto privilegiado e o código não confiável em jobs diferentes.
- Compile e teste em
pull_request, sem segredos. Se um passo privilegiado posterior for necessário, execute-o emworkflow_runsobre o artefato já armazenado, nunca sobre um checkout novo do branch do contribuidor. - Se o
pull_request_targetfor genuinamente necessário, não faça checkout do head ref de jeito nenhum. Um workflow que apenas etiqueta ou comenta não precisa do código. - Limite as credenciais ao job que precisa delas. Um token de registro somente leitura para instalações e um token de publicação separado que exista só no workflow de release teriam tornado esse incidente sobrevivível.
- Execute as instalações com os lifecycle scripts desabilitados, por exemplo
npm ci --ignore-scripts, para que umpackage.jsonnão confiável não consiga executar antes dos seus próprios comandos. - Exija um environment com aprovação manual para qualquer job que detenha uma credencial de publicação.