Translated ['', 'src/pentesting-ci-cd/github-security/abusing-github-act

This commit is contained in:
Translator
2026-07-06 15:31:01 +00:00
parent 7df12caf19
commit 5e32315c93
@@ -2,20 +2,20 @@
{{#include ../../../banners/hacktricks-training.md}}
## Verständnis des Risikos
## Understanding the risk
GitHub Actions wertet Ausdrücke ${{ ... }} aus, bevor der Schritt ausgeführt wird. Der ausgewertete Wert wird in das Programm des Schritts eingefügt (bei run-Schritten ein Shell-Skript). Wenn untrusted input direkt in run: interpoliert wird, kontrolliert der Angreifer einen Teil des Shell-Programms und kann beliebige Befehle ausführen.
GitHub Actions rendert Ausdrücke ${{ ... }} vor der Ausführung des Steps. Der gerenderte Wert wird in das Programm des Steps eingefügt (bei run-steps ein shell script). Wenn du nicht vertrauenswürdige Eingaben direkt in run: einfügst, kontrolliert der Angreifer einen Teil des shell-Programms und kann beliebige Commands ausführen.
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
Wichtige Punkte:
- Das Rendern erfolgt vor der Ausführung. Das run-Skript wird mit allen aufgelösten Ausdrücken generiert und anschließend von der Shell ausgeführt.
- Viele contexts enthalten vom Benutzer kontrollierte Felder, abhängig vom auslösenden Ereignis (issues, PRs, comments, discussions, forks, stars, etc.). Siehe die untrusted input reference: https://securitylab.github.com/resources/github-actions-untrusted-input/
- Shell quoting innerhalb von run: ist keine verlässliche Verteidigung, da die injection in der Phase des Template-Renderings auftritt. Angreifer können aus Anführungszeichen ausbrechen oder Operatoren durch manipulierte Eingaben injizieren.
Key points:
- Das Rendering passiert vor der Ausführung. Das run script wird mit allen aufgelösten Expressions generiert und dann von der shell ausgeführt.
- Viele contexts enthalten je nach Trigger-Event vom Benutzer kontrollierbare Felder (issues, PRs, comments, discussions, forks, stars, etc.). Siehe die untrusted input reference: https://securitylab.github.com/resources/github-actions-untrusted-input/
- Shell-Quoting innerhalb von run: ist keine zuverlässige Defense, weil die injection auf der Template-Rendering-Ebene passiert. Angreifer können durch präparierte Eingaben aus Quotes ausbrechen oder Operators injizieren.
## Verwundbares Muster → RCE auf dem Runner
## Vulnerable pattern → RCE on runner
Verwundbarer Workflow (ausgelöst, wenn jemand ein neues issue öffnet):
Vulnerable workflow (triggered when someone opens a new issue):
```yaml
name: New Issue Created
on:
@@ -36,20 +36,56 @@ with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: new
```
Wenn ein Angreifer ein Issue mit dem Titel $(id) eröffnet, wird der gerenderte Schritt:
Wenn ein Angreifer ein Issue mit dem Titel $(id) öffnet, wird der gerenderte Schritt zu:
```sh
echo "New issue $(id) created"
```
Die Kommando-Substitution führt id auf dem Runner aus. Beispielausgabe:
Die command substitution führt id auf dem runner aus. Beispielausgabe:
```
New issue uid=1001(runner) gid=118(docker) groups=118(docker),4(adm),100(users),999(systemd-journal) created
```
Warum Quoting Sie nicht schützt:
- Ausdrücke werden zuerst gerendert, und das resultierende Skript wird danach ausgeführt. Wenn der unsichere Wert $(...), `;`, `"`/`'` oder Zeilenumbrüche enthält, kann er die Programmstruktur trotz Ihres Quoting verändern.
Warum Anführungszeichen dich nicht schützen:
- Expressions werden zuerst gerendert, dann läuft das daraus entstehende script. Wenn der nicht vertrauenswürdige Wert $(...), `;`, `"`/`'` oder Zeilenumbrüche enthält, kann er trotz deiner Anführungszeichen die Programmstruktur verändern.
## Sicheres Muster (shell variables via env)
## Comment-State-Verwechslung: gefälschte bot comments → shell injection
Korrekte Gegenmaßnahme: Kopieren Sie die unsichere Eingabe in eine Umgebungsvariable und verwenden Sie dann die native Shell-Erweiterung ($VAR) im run-Skript. Binden Sie nicht erneut mit ${{ ... }} innerhalb des Befehls ein.
Eine gefährliche Variante tritt auf, wenn ein workflow **comments durchsucht und das zurückgegebene comment später als vertrauenswürdigen automation state behandelt**. Zum Beispiel kann `peter-evans/find-comment` per `body-includes` suchen und den passenden `comment-body` als step output bereitstellen. Wenn der workflow **nicht** zusätzlich `comment-author` einschränkt, kann jeder user, der commenten darf, den von einem bot erwarteten marker text fälschen.
```yaml
- uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ github.event.issue.number }}
body-includes: "Opened a new issue in org/repo:"
```
Wenn diese Ausgabe später in Shell-Syntax eingebettet wird, wird der Workflow ausnutzbar, obwohl die ursprüngliche Quelle „nur ein Kommentar“ war:
```yaml
- run: |
if [ '${{ steps.fc.outputs.comment-body }}' = '' ]; then
echo "new issue needed"
fi
```
Ein Angreifer kann einen Kommentar posten, der beides:
- den gesuchten Marker-String enthält und
- shell-breaking content wie `' ]; <cmd>; if [ 'x` enthält
Nachdem GitHub `${{ ... }}` rendert, erhält Bash attacker-controlled Syntax, keine Daten. Das erzeugt einen **two-stage exploit**:
1. **Provenance confusion**: der Workflow verwechselt Angreifer-Kommentare mit Bot-Zustand.
2. **Script injection**: der zurückgegebene `comment-body` wird in `run:` eingefügt und ausgeführt.
### TOCTOU race gegen Bot-Kommentare
Wenn der legitime Bot-Kommentar erst nach einem früheren Schritt erstellt wird, kann ein Angreifer ihn aus dem Rennen drängen, indem er zuerst den gefälschten Kommentar postet. Wenn die search action den Kommentar des Angreifers zurückgibt, bevor der echte Bot-Kommentar existiert (oder bevor er ausgewählt wird), kann ein Kommentator mit niedrigen Rechten einen `issue_comment`/issue workflow in privilegierte runner execution verwandeln.
### Sicherere patterns für kommentargetriebene automation
- Wenn `find-comment` verwendet wird, erfordern Sie **sowohl content als auch provenance** (`comment-author`, repository/App identity oder eine andere starke Bindung).
- Verwenden Sie Kommentare nicht als state, wenn ein label, artifact, issue field oder externer datastore denselben state sicherer speichern kann.
- Fügen Sie `comment-body`, issue titles, labels oder andere workflow outputs, die davon abgeleitet sind, niemals direkt in `run:` ein.
- Wenn Sie comment text verwenden müssen, geben Sie ihn über `env:` oder eine Datei weiter und behandeln Sie ihn nur als data.
## Safe pattern (shell variables via env)
Korrekte mitigation: untrusted input in eine environment variable kopieren und dann native shell expansion ($VAR) im run script verwenden. Nicht mit ${{ ... }} innerhalb des Befehls erneut einbetten.
```yaml
# safe
jobs:
@@ -62,13 +98,13 @@ TITLE: ${{ github.event.issue.title }}
run: |
echo "New issue $TITLE created"
```
Hinweise:
- Avoid using ${{ env.TITLE }} inside run:. That reintroduces template rendering back into the command and brings the same injection risk.
- Prefer passing untrusted inputs via env: mapping and reference them with $VAR in run:.
Notes:
- Vermeide die Verwendung von ${{ env.TITLE }} innerhalb von run:. Das bringt die Template-Renderung wieder zurück in den command und führt zum selben Injection-Risiko.
- Bevorzuge das Übergeben nicht vertrauenswürdiger Inputs über env: mapping und referenziere sie in run: mit $VAR.
## Vom Leser auslösbare Angriffsflächen (als nicht vertrauenswürdig behandeln)
## Reader-triggerable surfaces (treat as untrusted)
Accounts mit nur Lesezugriff auf public repositories können trotzdem viele Events auslösen. Jedes Feld in Contexts, die aus diesen Events abgeleitet werden, muss als von einem Angreifer kontrolliert betrachtet werden, sofern nicht das Gegenteil bewiesen ist. Beispiele:
Accounts mit nur read permission auf public repositories können dennoch viele events auslösen. Jedes Feld in contexts, das von diesen events abgeleitet ist, muss als attacker-controlled betrachtet werden, sofern nicht anders bewiesen. Beispiele:
- issues, issue_comment
- discussion, discussion_comment (orgs can restrict discussions)
- pull_request, pull_request_review, pull_request_review_comment
@@ -77,17 +113,25 @@ Accounts mit nur Lesezugriff auf public repositories können trotzdem viele Even
- watch (starring a repo)
- Indirectly via workflow_run/workflow_call chains
Welche konkreten Felder angreifer-kontrolliert sind, ist eventspezifisch. Siehe GitHub Security Labs untrusted input guide: https://securitylab.github.com/resources/github-actions-untrusted-input/
Welche spezifischen Felder attacker-controlled sind, ist event-spezifisch. Konsultiere den GitHub Security Labs untrusted input guide: https://securitylab.github.com/resources/github-actions-untrusted-input/
## Lokale Validierung ohne das target repo zu berühren
Du kannst viele GitHub Actions script injections sicher mit [`act`](https://github.com/nektos/act) reproduzieren: erzeug eine synthetische event JSON, führe den verwundbaren workflow lokal aus und ersetze die Ausgabe der externen action durch einen kontrollierten Wert (zum Beispiel einen gemockten `comment-body`). Das ist nützlich, um die payload-Struktur zu debuggen, zu verifizieren, ob der injected text immer noch gültige Bash-Syntax hinterlässt, und harmloses canary exfiltration vor jedem Live-Test zu bestätigen.
## Praktische Tipps
- Minimize use of expressions inside run:. Prefer env: mapping + $VAR.
- If you must transform input, do it in the shell using safe tools (printf %q, jq -r, etc.), still starting from a shell variable.
- Minimiere die Verwendung von expressions innerhalb von run:. Bevorzuge env: mapping + $VAR.
- Wenn du Input transformieren musst, mache es in der shell mit sicheren tools (printf %q, jq -r, etc.), ausgehend von einer shell variable.
- Sei besonders vorsichtig, wenn du branch names, PR titles, usernames, labels, discussion titles und PR head refs in scripts, command-line flags oder file paths interpolierst.
- Für reusable workflows und composite actions gilt dasselbe Muster: map to env then reference $VAR.
- Für reusable workflows und composite actions gilt dasselbe Muster: nach env mappen und dann $VAR referenzieren.
## References
- [Find Comment, Get Shell: Command Injection in dbts GitHub Actions](https://landh.tech/blog/20260701-find-comment-get-shell)
- [peter-evans/find-comment](https://github.com/peter-evans/find-comment)
- [GHSL-2023-109: GitHub Actions command injection in a TDesign Vue Next workflow](https://securitylab.github.com/advisories/GHSL-2023-109_TDesign_Vue_Next/)
- [nektos/act](https://github.com/nektos/act)
- [GitHub Actions: A Cloudy Day for Security - Part 1](https://binarysecurity.no/posts/2025/08/securing-gh-actions-part1)
- [GitHub workflow syntax](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions)
- [Contexts and expression syntax](https://docs.github.com/en/actions/learn-github-actions/contexts)