Rekonstruktion des Kompromisses bei der GitHub Action „changed-files“ von TJ Actions
17. März 2025
0 Min. LesezeitAm Freitagnachmittag, dem 14. März 2025, wurden erste Einzelheiten zu einem schwerwiegenden Sicherheits-Exploit in einer beliebten GitHub Action namens changed files (tj-actions/changed-files) bekannt. Rund 23.000 GitHub-Repositories nutzen diese Action in ihren CI- und DevOps-Workflows. Damit lässt sich nachverfolgen, welche Dateien sich zwischen Branches und Commits geändert haben.
Ein Angreifer mit Schreibberechtigungen für das Action-Repository erstellte einen Commit, durch den verschlüsselte Secrets in den GitHub-Action-Logs im Klartext erschienen. Dies hätte zu einem verheerenden Sicherheitsvorfall in einem öffentlichen Repository führen können, da auch die Action-Logs öffentlich sind.
Zunächst möchten wir unseren Kunden versichern, dass Snyk eine Bewertung unserer Infrastruktur abgeschlossen hat. Dabei konnten wir bestätigen, dass wir keine betroffene Version der anfälligen Bibliothek verwenden. Snyk ist daher vor dieser Schwachstelle geschützt.
Dennoch möchten wir Ihnen Einblicke in die eingehende Analyse der Schwachstelle durch Snyk geben, erklären, wie sie funktioniert, und sinnvolle Gegenmaßnahmen vorstellen. Darum geht es in diesem Blogbeitrag.
Zum Zeitpunkt der Veröffentlichung ist nicht bekannt, warum der Angreifer Schreibberechtigungen für das Repository hatte, in dem sich der Code dieser GitHub Action befindet. Die folgenden Faktoren ermöglichten den Angriff zusätzlich zu den Schreibberechtigungen:
Vorhandene Release-Tags so ändern, dass sie auf den Angriffs-Commit verweisen
Den Angriffs-Commit von allen Branches, einschließlich des Main-Branches, abkoppeln
Diese Faktoren verschleierten den Angriff ein Stück weit. Der Angriff beruhte darauf, einen externen Netzwerkaufruf zum Herunterladen des Angriffscodes auszuführen. Dadurch wurde er letztlich von Diensten entdeckt, die ungewöhnliche Netzwerkaktivitäten überwachen.
Gegen Ende dieses Beitrags führe ich den Angriff (auf ungefährliche Weise) nach, damit Sie besser verstehen, wie er möglich war und wie Sie sich davor schützen können.
Verwaiste Git-Commits
Probieren Sie es als Übung aus: Erstellen Sie ein neues Repository auf GitHub und klonen Sie es lokal. Erstellen Sie einen Branch und pushen Sie ihn zu GitHub. Notieren Sie sich den Commit-Hash. Löschen Sie anschließend den Remote-Branch. Sie können diesen Commit weiterhin aufrufen, obwohl er keinem vorhandenen Branch mehr zugeordnet ist.
Hier die oben beschriebenen Schritte:
1. Erstellen Sie ein Repository auf GitHub
2. Erstellen Sie ein lokales Repository und fügen Sie das gerade erstellte GitHub-Remote hinzu
3. Erstellen Sie einen Branch
4. Fügen Sie einen Commit hinzu und pushen Sie ihn
5. Notieren Sie sich den Commit auf GitHub

6. Löschen Sie den Branch auf GitHub

7. Rufen Sie die URL des zuvor gespeicherten Commits auf

So sieht ein verwaister Branch aus. Er ist einer der entscheidenden Faktoren, die den Angriff auf den Workflow der GitHub Action changes-files ermöglichten.
Der andere entscheidende Faktor war das Verschieben von Release-Tags im Repository.
GitHub-Release-Tags
Entwicklerinnen und Entwickler messen GitHub-Release-Tags leider manchmal eine besondere Bedeutung bei. Wenn uns gesagt wird, wir sollen v35 eines bestimmten Releases verwenden, verweisen wir auf dieses Tag und gehen davon aus, genau diese Version zu erhalten. Unter normalen Umständen ist das eine vernünftige Annahme. GitHub-Tags sind jedoch lediglich praktische Zeichenfolgen, die auf einen bestimmten Commit verweisen – ohne Magie, selbst bei sorgfältiger semantischer Versionierung. Hier sehen Sie einen Auszug aus einer GitHub-Action-YAML-Datei, die auf die Action changes-files verweist:
Entscheidend ist die letzte Zeile, in der auf ein bestimmtes Tag des GitHub-Action-Repositorys verwiesen wird. Der Angreifer löschte die ursprünglichen Versions-Tags und verschob sie auf den bösartigen Commit, der verwaist war. So leitete er mehrere vorhandene Release-Tags auf seinen schädlichen Commit um und vergrößerte damit die Auswirkungen des Angriffs.
Anhand des vorherigen Beispiels lässt sich das veranschaulichen. Auf meinem lokalen Rechner tagge ich den Branch, an dem ich gearbeitet habe:
Auf GitHub ist nun ein Tag mit dem verwaisten Commit verknüpft. Ich kann folgende Adresse aufrufen:
Dann wird Folgendes angezeigt:

Der Angreifer konnte also Menschen auf den bösartigen Commit „verweisen“, indem er einfach das Repository des Codes der GitHub Action changed-files aktualisierte.
Entscheidend ist: Ein böswilliger Akteur hatte Schreibzugriff auf ein Repository, auf das sich 23.000 andere Repositorys verließen. Der Angreifer verschleierte seine Aktivitäten zwar geschickt, doch ohne Schreibzugriff hätte er den Angriff niemals durchführen können.
Sehen wir uns genauer an, worauf es der Angreifer abgesehen hatte.
Secrets offenlegen
Secrets werden häufig benötigt, damit Build-Skripte mit anderen Diensten interagieren oder notwendige Schlüssel für Builds und Tests bereitstellen können.
GitHub verfügt über ein ausgeklügeltes Verschlüsselungsverfahren zur Verwaltung von Secrets. Sie können ein Secret auf Repository-Ebene festlegen und in Ihrem Build-Skript darauf zugreifen, ohne dass es jemals im Build-Log erscheint oder offengelegt wird. Im Hintergrund entschlüsselt GitHub das Secret und stellt es Ihrem Build-Skript als Referenz zur Verfügung. In der Praxis könnte eine Zeile in Ihrer GitHub-Action-YAML-Datei etwa so aussehen:
Der restliche Teil des Skripts kann nun auf das Secret zugreifen, ohne dass es im Skript selbst oder in den Logs erscheinen muss.
Der Angreifer änderte TJs GitHub Action changed-files, sodass ein Skript von einem entfernten Speicherort heruntergeladen, auf der virtuellen Maschine der laufenden GitHub Action lokal gespeichert und anschließend ausgeführt wurde. Das geschah in zwei Schritten. Der erste war der bösartige, inzwischen gelöschte verwaiste Commit. Hier sehen Sie den entscheidenden Teil dieses Git-Commits:
Wenn Sie die obige lange Zeichenfolge wie im Skript mit Base64 decodieren, erhalten Sie:
Das dort verlinkte Gist wurde inzwischen entfernt. Hier ist jedoch das Python-Skript, das sich darin befand:
Das obige Skript wurde mit Root-Rechten und dem Befehl sudo ausgeführt. Es durchsuchte den Prozessspeicher nach entschlüsselten Secrets und gab sie im Actions-Log aus. Das ist äußerst schädlich, denn GitHub-Actions-Logs in öffentlichen GitHub-Repositories sind standardmäßig ebenfalls öffentlich.
Ein Beitrag auf Hacker News (zu finden hier), der am 14. März veröffentlicht wurde, deutete darauf hin, dass StepSecurity den Exploit entdeckt hatte. Dieser Dienst überwacht unter anderem nicht autorisierte Netzwerkaufrufe in GitHub Actions. Der Autor und Maintainer der beliebten GitHub Action kommentierte den Beitrag und schilderte die Ereignisse. Als Erstes nannte er den Schreibzugriff des Angreifers auf das Repository.
Dank der schnellen Entdeckung des Exploits und der Reaktion des Maintainers wurde die Schwachstelle rasch behoben und der Angriff gestoppt. Nutzerinnen und Nutzer der GitHub Action changed-files sollten jedoch ihre Logs seit dem 14. März überprüfen, um sicherzustellen, dass keine Secrets in öffentlichen Actions-Logs offengelegt wurden.
Der Exploit in Aktion
Ich habe mehrere Repositorys eingerichtet, um diesen Exploit möglichst genau so nachzustellen, wie es der Angreifer konfiguriert hatte.
Im ersten Git-Repository ist die benutzerdefinierte GitHub Action definiert: tj-changed-files-action-goof.
Die Datei index.js ist unkompliziert und basiert größtenteils auf dem GitHub-Tutorial zum Erstellen von Actions:
Sie erwartet eine Eingabe namens who-to-greet und gibt Datum und Uhrzeit aus.
Das andere Repository enthält nur eine Datei: eine YAML-Datei, die so konfiguriert ist, dass sie die benutzerdefinierte GitHub Action ausführt. Außerdem ist im Repository ein Secret namens A_SECRET festgelegt. Das Repository heißt tj-changed-files-action-exploit-goof. In der Datei .github/workflows/main.yml sehen Sie Folgendes:
Dabei werden einige Dinge erledigt:
Eine „Hello, world!“-Nachricht ausgeben
Die GitHub Action
tj-changed-files-action-goofausführenDie von der Action ermittelte Uhrzeit ausgeben. Außerdem wird das Repository-Secret wie vorgesehen in einer GitHub Action verwendet.
Vergleichen Sie die Ausgabe von A Goof Step aus zwei verschiedenen Durchläufen. Zuerst diesen:
Als Nächstes diesen:
In beiden Fällen wird snyk-labs/tj-changed-files-action-goof@v1.0 ausgeführt. Beim zweiten Durchlauf sehen Sie die Ausgabe der kompromittierten GitHub Action. Decodieren wir die codierte Zeichenfolge zweimal mit Base64, erhalten wir:
In beiden Durchläufen sehen Sie, dass GitHub Actions den Wert des referenzierten Secrets schützt: a_secret: ***. Der Exploit-Code liest die Secrets jedoch als Klartext direkt aus dem Systemspeicher aus.
Wenn wir uns das Tag v1.0 im Repository tj-changed-files-action-goof noch einmal ansehen, erkennen wir einen ähnlichen verwaisten Branch wie beim ursprünglichen Exploit:

Ursprünglich verwies v1.0 auf den Branch main. Für die Simulation des Exploits habe ich das Tag jedoch einfach auf einen Branch attack umgeleitet und diesen anschließend verwaist:
Der letzte Kniff, den man leicht übersieht, ist der Befehl ncc build index.js. Er aktualisiert dist/index.js, und nur diese aktualisierte Datei wird committet. Die GitHub Action führt tatsächlich die mit Webpack gebündelte Datei dist/index.js aus. Wenn Sie sich also die Datei index.js im (verwaisten) Branch v1.0 ansehen, scheint sich nichts geändert zu haben.
Hier sehen Sie den Diff des Webpack-Codes in dist/index.js auf v1.0 im Vergleich zum Branch main.
Schützen Sie Ihre GitHub Actions
Ich bin sicher, dass weitere Details darüber bekannt werden, wie oder warum der Angreifer Schreibzugriff auf dieses beliebte GitHub-Action-Repository hatte.
Es ist gängige Praxis, sich auf Git-Tags als Referenzversionen verschiedener Bibliotheken zu verlassen, die wir verwenden.
Es gibt mehrere Möglichkeiten, wie sich dieser konkrete Exploit hätte verhindern lassen.
1. Verweisen Sie bei benutzerdefinierten, externen GitHub Actions direkt auf den Commit-Hash
Zum Beispiel könnten Sie Folgendes verwenden:
Damit wird direkt auf den Commit-Hash statt auf ein Tag verwiesen. Das ist zwar unüblich, würde aber sicherstellen, dass die ausgeführte Version der benutzerdefinierten GitHub Action tatsächlich der von Ihnen erwarteten Version entspricht.
2. Verwenden Sie zusätzliche benutzerdefinierte GitHub Actions, um unerwartete Netzwerkaufrufe zu erkennen. So wurde StepSecurity auf den Exploit aufmerksam.
Es ist ermutigend zu sehen, wie sich eine Community aus Open-Source-Mitwirkenden, Unternehmen und Einzelpersonen darauf stürzt, neue Exploits wie diesen zu finden und zu beheben.
Snyk hat bereits früher Forschungsergebnisse und Best Practices zu GitHub Actions veröffentlicht. Wir empfehlen Ihnen dringend, daraus zu lernen und Ihre DevSecOps- und Sicherheitspraktiken zu überprüfen:
Handlungsaufruf: Schwachstellen in GitHub Actions untersuchen
Eine sichere CI/CD-Pipeline mit GitHub Actions für Ihre Java-Anwendung erstellen
Entdecken Sie den Stand der Open-Source-Sicherheit
Erfahren Sie mehr über aktuelle Trends und Ansätze für Open-Source-Software und Supply-Chain-Sicherheit.
