Build-Caches sind ein Standardwerkzeug, um Pipelines schneller zu machen. Statt Abhängigkeiten, kompilierte Zwischenstände oder Docker-Layer bei jedem Lauf neu zu erzeugen, werden sie gespeichert und wiederverwendet. Das spart Minuten pro Build und bei vielen Läufen am Tag summiert sich das. Der Preis dafür wird selten benannt: Ein Cache ist geteilter, wiederverwendeter Zustand. Wer ihn manipulieren kann, beeinflusst nicht einen Build, sondern jeden folgenden, der ihn lädt.

Warum ein Cache anders ist als eine Abhängigkeit

Eine externe Abhängigkeit hat wenigstens eine Herkunft. Sie kommt aus einer Registry, hat eine Version, oft einen Hash im Lockfile. Ein Cache-Eintrag hat das meist nicht. Er ist ein Blob, der unter einem Schlüssel abgelegt wurde, und der Schlüssel entscheidet, ob er beim nächsten Lauf geladen wird. Wenn dieser Schlüssel vorhersagbar ist und der Cache über Branches oder Läufe hinweg geteilt wird, entsteht ein Kanal, über den Inhalt aus einem Kontext in einen anderen wandert. Genau das ist der Kern des Problems.

Wie ein Cache vergiftet wird

Der klassische Weg führt über einen Pull Request. In vielen Setups darf ein Build aus einem Fork oder einem Feature-Branch in denselben Cache schreiben, den später der Hauptbranch liest. Ein Angreifer öffnet also einen unscheinbaren PR, sein Build füllt den Cache mit einer präparierten Abhängigkeit oder einem manipulierten Zwischenartefakt, und beim nächsten Lauf auf main wird dieser Inhalt geladen und ausgeführt. Der schädliche Code steht nie im Repository. Er lebt im Cache und wird durch einen legitimen Build eingeschleust.

Der zweite Weg ist die Schlüssel-Kollision. Wenn der Cache-Key nur auf dem Namen einer Lockfile beruht, aber nicht auf deren Inhalt, kann ein alter oder fremder Eintrag unter demselben Schlüssel liegen. Der Build lädt ihn, ohne zu prüfen, ob er zum aktuellen Stand passt. Reproduzierbarkeit bricht an dieser Stelle, und mit ihr die Möglichkeit, nachträglich zu sagen, was eigentlich gebaut wurde.

Was den Schaden begrenzt

Die wirksamste Grenze ist die Trennung. Caches aus nicht vertrauenswürdigen Kontexten dürfen nicht in vertrauenswürdige zurückfließen. Bei GitHub Actions bedeutet das konkret: Ein Cache, den ein Pull Request beschreibt, ist an dessen Scope gebunden und wird auf dem Zielbranch nicht wiederverwendet. Diese Isolation ist voreingestellt, lässt sich aber durch eigene Konstruktionen aushebeln, etwa wenn man Caches manuell über Workflows hinweg teilt oder externe Cache-Backends ohne Scope-Prüfung einbindet.

Die zweite Grenze ist der Schlüssel. Ein Cache-Key sollte den Inhalt widerspiegeln, den er repräsentiert. Ein Hash über die Lockfile statt nur ihres Namens sorgt dafür, dass ein veränderter Abhängigkeitsstand einen neuen Schlüssel erzeugt und ein alter Eintrag nicht fälschlich geladen wird. Das ist derselbe Gedanke wie bei reproduzierbaren Builds: gleicher Input, gleicher Output, und jede Abweichung wird sichtbar.

Die dritte Grenze ist Behandeln des Cache-Inhalts als das, was er ist: nicht vertrauenswürdige Daten. Ein aus dem Cache geladenes Binary ist kein geprüftes Artefakt, nur weil es schnell verfügbar war. Wo es auf Integrität ankommt, gehört die Verifikation gegen einen bekannten Hash oder eine Signatur davor, nicht der blinde Wiederverwendungs-Reflex.

Der praktische Blick

Die meisten Teams stolpern nicht über ein exotisches Angriffsszenario, sondern über eine bequeme Abkürzung: ein geteilter Cache über alle Branches, ein Schlüssel, der nur grob passt, ein selbstgebautes Cache-Backend ohne Scope. Jede dieser Entscheidungen ist für sich harmlos und in Summe öffnen sie den Kanal. Die Prüfung ist überschaubar: Wer darf in welchen Cache schreiben, worauf beruht der Schlüssel, und was passiert mit dem geladenen Inhalt bevor er ausgeführt wird. Wer Wert auf Nachvollziehbarkeit legt, sollte den Cache genauso behandeln wie den Rest der Lieferkette, denn er ist Teil davon. Wie derselbe Code zweimal dasselbe Ergebnis liefern sollte, steht in unserem Beitrag zu reproduzierbaren Builds. Warum Lockfiles zwischen Reproduzierbarkeit und Sicherheit vermitteln, haben wir separat beschrieben.


Prüfen, wo Ihr Cache offen steht

Der GitSecOps QuickCheck sieht sich Ihre Pipeline auf genau solche stillen Kanäle an: geteilte Caches, schwache Schlüssel, fehlende Trennung zwischen vertrauenswürdigen und fremden Läufen. In drei Werktagen, für 1.990 Euro netto, mit konkreten Befunden statt allgemeiner Hinweise. Kontakt: mm@crank.zone.

Hinterlasse einen Kommentar

MHM Digitale Lösungen

Wir machen Software-Lieferketten sicher. GitSecOps prüft und härtet deine Build- und Deployment-Pipeline, damit du gegenüber Kunden und Prüfern bestehst. Hier schreiben wir nüchtern über Supply Chain Security, DevSecOps und digitales Business.

Kontakt