ATALAIA
  1. Início
  2. Recursos
  3. Blog
  4. Fixe as suas actions por SHA: uma correção de uma tarde
Pipeline · Guia

Fixe as suas actions por SHA: uma correção de uma tarde

As tags mudam, os SHAs não. Faça pin de cada action, deixe um bot mantê-las atualizadas e reduza o token ao que cada job precisa.

3 min de leituraEquipa Atalaia

GitHub ActionsCI/CDHardening

A maioria dos ficheiros de workflow referencia actions como actions/checkout@v4. Parece uma versão, mas é uma tag git, e quem controla o repositório pode apontar uma tag para qualquer commit que quiser, incluindo um que não existia ontem. O seu pipeline corre então o código novo com os seus secrets, no push seguinte, sem uma única linha alterada no seu repositório.

A solução não é glamorosa e não precisa de uma ferramenta nova. É uma tarde de pinning, um pequeno ficheiro de configuração e um par de linhas de permissões. Esta é a ordem pela qual o faríamos.

Porque é que as tags são o ponto fraco

Uma referência por tag é resolvida em tempo de execução. Se um atacante obtiver acesso de escrita ao repositório de uma action, ou à conta de um maintainer, pode fazer force-push de uma tag para um commit malicioso e todos os workflows que usam essa tag passam a usá-lo. Foi exatamente o que aconteceu no compromisso do tj-actions/changed-files em março de 2025, em que versões com novas tags despejaram segredos de CI nos logs de build.

Um SHA de commit completo, com 40 caracteres, é diferente. Identifica uma árvore exata de ficheiros. Ninguém o consegue fazer apontar para outro sítio. SHAs curtos e nomes de branches não contam: só o hash completo é imutável.

Faça pin de cada action de terceiros

Primeiro descubra o que está a usar. Isto lista todas as linhas uses: que ainda não são um SHA completo, sem contar as actions locais:

grep -rn "uses:" .github/workflows \
  | grep -v "./" \
  | grep -Ev "@[0-9a-f]{40}"

Pode resolver uma tag à mão com git ls-remote, mas uma ferramenta é mais rápida e menos sujeita a erros. O pinact, open-source, reescreve as tags para SHAs e mantém a versão como comentário no fim, para que as pessoas ainda a consigam ler:

pinact run

# before
- uses: actions/setup-node@v4
# after
- uses: actions/setup-node@<full-40-char-sha> # v4.1.0

Dois casos limite. Os steps baseados em Docker que usam docker:// devem ter pin por image digest, não por tag. E os reusable workflows chamados a partir de outros repositórios precisam do mesmo tratamento que as actions.

Deixe um bot manter os pins atualizados

A objeção habitual ao pinning é que deixa de receber correções. Não deixa, se o Dependabot vigiar o ecossistema github-actions. Ele percebe pins por SHA com comentários de versão e abre um pull request que atualiza ambos.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns: ["*"]

Agrupar mantém tudo num pull request por semana em vez de uma dúzia. Reveja-o como qualquer outra alteração de dependências: leia o diff da action quando o salto é grande ou o publisher é pequeno.

Reduzir o GITHUB_TOKEN

O pinning limita o código que corre. As permissões limitam o que esse código pode fazer se algo ainda correr mal. Defina o valor por omissão da organização ou do repositório como só de leitura, depois declare as permissões no topo de cada workflow e alargue-as apenas no job que precisa:

permissions:
  contents: read

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write

Um permissions: {} vazio também é válido, para jobs que só fazem lint ou testes. Já que está aqui, procure chaves de cloud de longa duração nos secrets e substitua-as por federação OIDC onde o seu fornecedor o suporte.

A armadilha do pull_request_target

pull_request_target corre no contexto do repositório base, com acesso a segredos e a um token que pode escrever. Existe para que os workflows possam pôr labels ou comentar em pull requests vindos de forks. A armadilha é fazer checkout do código do fork e corrê-lo:

on: pull_request_target
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<sha>
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm install && npm test   # runs attacker code with your secrets

O mesmo vale para interpolar campos não confiáveis, como o título de um pull request, diretamente num script run:. Passe-os por uma variável de ambiente, para que a shell os trate como dados.

Verifique o seu em 10 minutos

  1. Corra o grep acima e conte as referências sem pin.
  2. Corra zizmor .github/workflows. Sinaliza actions sem pin, permissões excessivas, triggers arriscados e template injection.
  3. Corra actionlint para apanhar os erros de sintaxe que o pinning por vezes introduz.
  4. Verifique a definição do repositório para as permissões por omissão dos workflows e defina-a como read.
  5. Adicione o ficheiro do Dependabot e faça merge da primeira atualização agrupada.

Fica feito numa tarde. A partir daí, cada alteração ao que corre no seu pipeline chega como um pull request revisto, e não como um retag silencioso.

Onde isto fica na linha

Na torre (em desenvolvimento):

Vamos conversar

Uma chamada de 30 minutos. Sem slides, sem tabela de preços, e um próximo passo em qualquer caso.