8.1 KiB
Gh Actions - Context Script Injections
{{#include ../../../banners/hacktricks-training.md}}
Entendendo o risco
GitHub Actions renderiza expressões ${{ ... }} antes de a step executar. O valor renderizado é colado no programa da step (para run steps, um shell script). Se você interpolar input não confiável diretamente dentro de run:, o atacante controla parte do shell program e pode executar comandos arbitrários.
Docs: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions and contexts/functions: https://docs.github.com/en/actions/learn-github-actions/contexts
Pontos-chave:
- A renderização acontece antes da execução. O run script é gerado com todas as expressões resolvidas e então executado pelo shell.
- Muitos contexts contêm campos controlados pelo usuário dependendo do evento que dispara a ação (issues, PRs, comments, discussions, forks, stars, etc.). Veja a referência de untrusted input: https://securitylab.github.com/resources/github-actions-untrusted-input/
- Shell quoting dentro de run: não é uma defesa confiável, porque a injection ocorre na etapa de template rendering. Attackers podem sair de quotes ou injetar operators via input criado para isso.
Padrão vulnerável → RCE on runner
Workflow vulnerável (disparado quando alguém abre uma nova issue):
name: New Issue Created
on:
issues:
types: [opened]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: New issue
run: |
echo "New issue ${{ github.event.issue.title }} created"
- name: Add "new" label to issue
uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: new
Se um atacante abrir um issue intitulado $(id), o step renderizado se torna:
echo "New issue $(id) created"
A substituição de comando executa id no runner. Exemplo de saída:
New issue uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),100(users),999(systemd-journal) created
Por que aspas não salvam você:
- Expressions são renderizadas primeiro, depois o script resultante é executado. Se o valor não confiável contiver $(...),
;,"/', ou quebras de linha, ele pode alterar a estrutura do programa apesar das aspas.
Confusão de estado de comentário: comentários spoofed de bot → shell injection
Uma variante perigosa aparece quando um workflow procura comments e depois trata o comentário retornado como estado confiável de automação. Por exemplo, peter-evans/find-comment pode pesquisar por body-includes e expor o comment-body correspondente como output de uma step. Se o workflow não restringir também comment-author, qualquer usuário que possa comentar pode spoof the marker text esperado de um bot.
- uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ github.event.issue.number }}
body-includes: "Opened a new issue in org/repo:"
Se essa saída for posteriormente incorporada na sintaxe do shell, o workflow se torna explorável, mesmo que a fonte original fosse "apenas um comentário":
- run: |
if [ '${{ steps.fc.outputs.comment-body }}' = '' ]; then
echo "new issue needed"
fi
Um atacante pode postar um comentário que:
- corresponda à string marker pesquisada, e
- contenha conteúdo que quebre o shell, como
' ]; <cmd>; if [ 'x
Depois que o GitHub renderiza ${{ ... }}, o Bash recebe sintaxe controlada pelo atacante, não dados. Isso cria um exploit em duas etapas:
- Confusion of provenance: o workflow confunde comentários do atacante com estado do bot.
- Script injection: o
comment-bodyretornado é colado emrun:e executado.
TOCTOU race against bot comments
Se o comentário legítimo do bot só for criado após alguma etapa anterior, um atacante pode ganhar a corrida postando primeiro o comentário falsificado. Se a ação de busca retornar o comentário do atacante antes de o comentário real do bot existir (ou antes de ele ser selecionado), um comentarista público com baixo privilégio pode transformar um workflow issue_comment/issue em execução privilegiada no runner.
Safer patterns for comment-driven automation
- Ao usar
find-comment, exija tanto conteúdo quanto provenance (comment-author, identidade do repository/App, ou outro vínculo forte). - Não use comentários como estado se um label, artifact, campo da issue ou datastore externo puder armazenar o mesmo estado com mais segurança.
- Nunca cole
comment-body, títulos de issue, labels ou qualquer saída do workflow derivada deles diretamente emrun:. - Se você precisar consumir texto de comentário, passe-o via
env:ou um file e trate-o apenas como dados.
Safe pattern (shell variables via env)
Mitigação correta: copie a entrada não confiável para uma environment variable, e então use a expansão nativa do shell ($VAR) no run script. Não re-incorpore com ${{ ... }} dentro do comando.
# safe
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: New issue
env:
TITLE: ${{ github.event.issue.title }}
run: |
echo "New issue $TITLE created"
Notes:
- Evite usar ${{ env.TITLE }} dentro de run:. Isso reintroduz a renderização de template de volta ao command e traz o mesmo risco de injection.
- Prefira passar inputs não confiáveis via env: mapping e referenciá-los com $VAR em run:.
Reader-triggerable surfaces (trate como não confiáveis)
Accounts com apenas permissão de leitura em repositórios públicos ainda podem acionar muitos events. Qualquer campo em contexts derivados desses events deve ser considerado controlado por attacker, a menos que se prove o contrário. Examples:
- issues, issue_comment
- discussion, discussion_comment (orgs podem restringir discussions)
- pull_request, pull_request_review, pull_request_review_comment
- pull_request_target (perigoso se usado incorretamente, executa no contexto do base repo)
- fork (qualquer pessoa pode fazer fork de repositórios públicos)
- watch (marcar um repo com star)
- Indiretamente via workflow_run/workflow_call chains
Quais campos específicos são controlados por attacker é algo específico de cada event. Consulte o guia de untrusted input do GitHub Security Lab: https://securitylab.github.com/resources/github-actions-untrusted-input/
Local validation without touching the target repo
Você pode reproduzir muitas script injections de GitHub Actions com segurança usando act: gere um synthetic event JSON, execute o vulnerable workflow localmente e substitua a saída da external action por um valor controlado (por example, um comment-body mockado). Isso é útil para depurar a payload structure, verificar se o texto injected ainda deixa uma sintaxe válida de Bash e confirmar uma harmless canary exfiltration antes de qualquer live test.
Practical tips
- Minimize o uso de expressions dentro de run:. Prefira env: mapping + $VAR.
- Se precisar transformar input, faça isso no shell usando safe tools (printf %q, jq -r, etc.), ainda começando a partir de uma shell variable.
- Tenha cuidado extra ao interpolar branch names, PR titles, usernames, labels, discussion titles e PR head refs em scripts, command-line flags ou file paths.
- Para reusable workflows e composite actions, aplique o mesmo padrão: mapear para env e então referenciar $VAR.
References
- Find Comment, Get Shell: Command Injection in dbt’s GitHub Actions
- peter-evans/find-comment
- GHSL-2023-109: GitHub Actions command injection in a TDesign Vue Next workflow
- nektos/act
- GitHub Actions: A Cloudy Day for Security - Part 1
- GitHub workflow syntax
- Contexts and expression syntax
- Untrusted input reference for GitHub Actions
{{#include ../../../banners/hacktricks-training.md}}