Zurück zu allen Beiträgen
Veröffentlicht am · von Renaud Deraison

Niemand hat eine neue Version veröffentlicht

Wordfences Advisory vom 9. August zur BdThemes-Kompromittierung beschreibt einen Supply-Chain-Angriff ganz ohne Supply Chain. Kein Release, kein Install-Skript, kein Typosquat, keine geänderte Datei im Plugin-Repository. Angreifer verschafften sich Schreibzugriff auf den Storage-Bucket des Herstellers und änderten einen JSON-Feed, den 350.000 Sites bei jedem Laden einer Admin-Seite abriefen. Für Entwickler lautet die Frage, die das aufwirft: Was darf die Maschine, auf der dein Coding-Agent läuft, schreiben, und für wen?

Jede Supply-Chain-Kontrolle, die du hast, prüft Dinge, die veröffentlicht werden. Diese hier hat nichts veröffentlicht und trotzdem 350.000 Sites erreicht.

Am 7. August 2026 bekam Wordfence erste Meldungen über WordPress-Sites mit BdThemes-Plugins, auf denen Administratorkonten wuchsen, die niemand angelegt hatte. Die Liste umfasst sieben Plugins: Element Pack Addons for Elementor, Prime Slider, Pixel Gallery, Ultimate Post Kit, Ultimate Store Kit, Live Copy Paste und Smart Admin Assistant. Element Pack allein hat über 100.000 aktive Installationen, die sieben zusammen überschreiten 350.000.

Bis zum 8. August hatte der Hersteller seine API bereinigt und WordPress.org hatte alle sieben Plugins bis zum Abschluss der Untersuchung zurückgezogen. Wordfence veröffentlichte seine PSA zur Kompromittierung am Tag darauf, und der zentrale Befund ist eine Verneinung: Niemand hatte den Plugin-Code angerührt.

Es wurde nichts veröffentlicht

BdThemes-Plugins enthalten eine interne Komponente namens Biggopti, deren Aufgabe klein und langweilig ist: Werbebanner von der Remote-API des Herstellers holen und im WordPress-Admin-Dashboard zeichnen. Die Bannerdaten sind eine statische JSON-Datei in einem DigitalOcean-Spaces-Bucket, ausgeliefert hinter Cloudflare.

Angreifer erlangten Schreibzugriff auf diesen Bucket. Das ist der gesamte Einbruch.

Biggopti nahm das Feld display_id aus der JSON-Antwort und ließ es unescaped in ein HTML-id-Attribut fallen, eine Cross-Site-Scripting-Lücke, die Wordfence mit CVSS 5.4, mittel bewertete und bis zum 1. März 2026 zurückverfolgte, als ein Entwickler sie einmal schrieb und von einem Plugin ins nächste kopierte. Eine display_id mit einem onanimationstart-Handler ist deshalb JavaScript, und sie läuft im Browser jedes eingeloggten Administrators, bei jedem Laden einer wp-admin-Seite, auf jeder Site mit installiertem Plugin.

Die Payload nutzte diesen Platz gut. Ein Skript namens w2.js fragte bei einem Command-and-Control-Server nach Zielvorgaben, also der Maschine, mit der ein Angreifer einer Payload sagt, welche Sites sich zu übernehmen lohnen, und legte dann über die WordPress-REST-API versteckte Administratorkonten an, auf dem Rücken der Sitzung und der Nonces des echten Administrators. Eine Variante leitete vorhersagbare Benutzernamen aus dem Hostnamen der Site ab. Ein falsches Plugin, oft mit einem Namen wie wp-smart-thumbnails, lieferte eine Web-Shell unter emer-run.php, also ein Skript, das ein Angreifer durch Aufruf einer ganz gewöhnlichen URL steuern kann. Must-use-Plugins, die WordPress bei jedem Request lädt und vor dem Plugin-Bildschirm verbirgt, trugen eine Magic-Login-Hintertür und ein Modul, das die neuen Konten verbarg, indem es die vom Admin-Bildschirm angezeigten Benutzersummen umschrieb.

Zeitstempel in den vergifteten Datensätzen legen den frühestmöglichen Beginn auf den 23. Juni 2026, sieben Wochen, bevor es jemandem auffiel.

Halte das gegen die Kontrollen, die ein sorgfältiges Team betreibt. Versions-Pinning: Die Versionen änderten sich nie. Lockfiles und Integritäts-Hashes: Sie passten, weil die Dateien, die sie abdeckten, unberührt waren. Prüfung der Install-Skripte: Es gab keine Installation. Typosquat-Erkennung: richtiger Name, richtiger Hersteller, richtiger Repository-Eintrag. Code-Review des Diffs zwischen Releases: kein Diff, weil kein Release. Zertifikat und CDN: beide bei bester Gesundheit. Cloudflare und TLS bewiesen, dass das JSON unverändert aus genau dem Bucket bei der Site ankam, aus dem es kommen sollte, und genau das machte es brauchbar.

Der Weg, den alle beobachtenWordPress.orgsieben Plugins, 350.000+aktive Installationenwas die Kontrollen prüfenVersionen, Hashes,Install-Skripte, das DiffUrteil: sauberund richtig. Keine Datei imRepository wurde je geändert.Der Weg, den niemand beobachteteder Bucket des HerstellersDigitalOcean Spaces,hinter Cloudflarejemand konnte hineinschreibenBiggopti holt das Bannerdisplay_id landet in einemHTML-id-Attribut, unescapedonanimationstart führt es ausin der Sitzung des Adminsversteckte Adminkonten via REST,emer-run.php-Web-Shell,Magic-Login in mu-pluginsfrühester vergifteter Datensatz: 23. Juni 2026Wordfence benachrichtigt: 7. August 2026Sieben Wochen, in denen jede Prüfung des veröffentlichten Artefakts bestand, und zu Recht bestand.
Zwei Wege in eine Site mit dem Plugin. Der veröffentlichte Weg, den jede Supply-Chain-Kontrolle prüft, hat sich nie geändert. Der Laufzeitweg holte eine JSON-Datei aus einem Bucket, in den jemand anderes schreiben konnte, und diese Datei kam als JavaScript im Browser des Administrators an.

Der Vorfall beginnt mit einem Zugangsdatum, das schreiben konnte

Liest man das Advisory von der Herstellerseite her, schrumpft die Angriffskette auf einen Schritt. Jemand erlangte Schreibzugriff auf einen Storage-Bucket. Alles danach ist Folge.

Upstream-Kompromittierungen kommen immer wieder in dieser Form. Der Wurm im npm-Scope von Red Hat, das Repository, das wirklich Microsoft gehörte, die Extension-Marktplätze, die Container-Registries: In jedem davon ist die Payload der Teil, über den geschrieben wird, und der Teil, auf den es ankam, war jemand, der am Ende Zugangsdaten hatte, mit denen sich ein Artefakt überschreiben lässt, das andere Leute regelmäßig abrufen.

Wo lebt so ein Zugangsdatum? In der Umgebung eines Entwicklers. Ein DIGITALOCEAN_ACCESS_TOKEN in einer Shell, eine doctl-Konfiguration in ~/.config, ein Registry-Login in ~/.docker/config.json, ein AWS-Profil, ein GitHub-Token in ~/.git-credentials, ein SSH-Schlüssel, der pushen kann. Auf einem Laptop, in einem Terminal, neben dem Projekt. 2026 ist neben dem Projekt auch der Ort, an dem dein Coding-Agent läuft.

Die meisten Beiträge in diesem Blog beschreiben einen Weg, diesen Agenten gegen dich zu wenden. Ein Bugreport, der einen Befehl ausführte. Eine Webseite, die die Konfiguration des Agenten umschrieb. Ein Kommentar, den niemand sehen konnte. Ein Skill, der mit deiner Identität lief. Drei Harness-Bypässe in einem Black-Hat-Vortrag. Nimm den Katalog als gegeben und nimm an, dass an irgendeinem Dienstag einer davon bei dir landet.

Wie schlimm dieser Dienstag wird, hängt an einer Frage, die du heute beantworten kannst, ohne zu wissen, welche Injektion gewinnt: Was darf diese Maschine schreiben, und für wen? Auf einem Laptop lautet die Antwort: alles, was du schreiben darfst, und wenn du irgendetwas ausliefert, gehört dazu das Artefakt, das mehrere hunderttausend Fremde ungeprüft abrufen.

Das Token ist nicht in der Maschine

Bromure Agentic Coding lässt den Agenten in einer wegwerfbaren Linux-VM auf Apple Silicon laufen, und jedes Byte seines Verkehrs kreuzt einen Proxy auf dem Host, außerhalb der Box, in der der Agent läuft. Bromure ordnet die Zugangsdaten um diese Grenze herum an, statt sie hineinzulegen.

Trag dein persönliches DigitalOcean-Zugangstoken im Credentials-Panel ein, und Bromure behält den echten Wert auf dem Mac. Was in der VM landet, ist eine Attrappe: eine Umgebungsvariable DIGITALOCEAN_ACCESS_TOKEN und eine ~/.config/doctl/config.yaml, die doctl ohne doctl auth init funktionieren lassen und einen Platzhalter enthalten. Wenn die VM eine Anfrage an api.digitalocean.com stellt, setzt der Host-Proxy auf der Leitung das echte Token ein, für dieses Ziel und für nichts sonst.

Alles, was in dieser VM läuft, kann die Umgebung lesen, die Dotfiles greppen und die ganze Platte ablaufen: der Agent, eine Abhängigkeit, ein Build-Schritt, ein Shell-Befehl aus einem vergifteten Issue. Was dabei zusammenkommt, ist brm_…. Dieselbe Anordnung deckt den Rest ab: GitHub-, GitLab- und Bitbucket-Tokens, Container-Registry-Logins, geschrieben als gefälschter Base64-Blob in ~/.docker/config.json, eine synthetische kubeconfig mit Wegwerf-Client-Zertifikaten, Linear, die HTTPS-Datenbank-Endpunkte und alles Weitere, was du unter Other API keys hinzufügst. AWS geht noch weiter: Der Host signiert jede Anfrage mit SigV4 und dem echten Secret neu, sodass ein Umgehen des Proxys eine InvalidSignatureException einbringt statt eines unautorisierten Schreibvorgangs.

Guardrails entscheidet über das Verb, auf dem Mac

Dieser Vorfall brauchte eine einzige Operation: ein Objekt in einem Bucket überschreiben. Guardrails ist eine Policy-Engine im Host-Proxy, mit einem Modus pro Ressource: Off, Block destructive oder Read-only. Stell DigitalOcean auf Read-only, und jede Mutation an api.digitalocean.com kommt als hartes 403 zurück, das der Agent als ganz gewöhnlichen API-Fehler meldet. Bromure trifft diese Entscheidung in macOS. Es gibt keine Einstellung in der VM, die man ändern könnte, keine Umgebungsvariable zum Löschen und keine Datei zum Bearbeiten.

Veröffentlichen fragt vorher

Jeder Zugangsdaten-Eintrag hat Require approval to use. Schalte es ein, und jeder Tausch Attrappe→echt löst auf dem Host einen Zustimmungsdialog aus, bevor der echte Wert auf die Leitung geht, mit zeitlich begrenzten Freigaben auf dem SSH-Pfad: fünf Minuten, eine Stunde, der Rest der Sitzung. Ein Release zu pushen ist etwas, das du vorhattest, und es kostet dich einen Klick. Ein Schreibvorgang, den du nicht gestartet hast, taucht als Dialog auf, den du nicht erwartet hast, in einem Moment, in dem du keinen erwartetest.

Registries und git bekommen dieselbe Behandlung

Guardrails klassifiziert Container-Registry-Verkehr nach Methode gegen den Hostnamen der Registry selbst: GET und HEAD sind ein Pull, PUT und POST sind ein Push, DELETE ist destruktiv. GitHub, GitLab und Bitbucket sind sowohl für die REST-API als auch für git über HTTPS abgedeckt, wo git-receive-pack als Schreibvorgang zählt und im Read-only-Modus blockiert wird, während Fetches immer durchgehen. Ein Profil, das deinen gesamten Abhängigkeitsgraphen liest und nichts pusht, sind zwei Einstellungen.

Die Umgebung ist eine Liste, die du geschrieben hast

Ein Bromure-Profil sieht die Mac-Ordner, die du mit ihm geteilt hast, bis zu acht, jeder unter /home/ubuntu eingehängt. Alles andere auf deinem Mac ist nicht eingehängt, nicht erreichbar und nicht auflistbar. Der Satz an Zugangsdaten ist der, den du diesem Profil hinzugefügt hast, sodass ein Profil, mit dem du ein Frontend baust, keine Cluster-Zugangsdaten enthält, die irgendetwas finden könnte, versteckt oder nicht.

Auf dem Laptop des EntwicklersDIGITALOCEAN_ACCESS_TOKEN=dop_v1_…~/.config/doctl, ~/.docker/config.json,~/.git-credentials, ~/.aws, ~/.sshder Agent wird gewendet, einmalein Issue, eine Seite, ein README, eine Logzeilejetzt liest er, was du liestein PUT auf einen Bucketund das Artefakt, das deine Nutzerautomatisch abrufen, gehört ihnenIn einem Bromure-ProfilDIGITALOCEAN_ACCESS_TOKEN=brm_…jedes Dotfile ist da und jedesSecret darin ist ein Platzhalterder echte Wert lebt auf dem Macauf der Leitung vom Host-Proxy getauscht,für api.digitalocean.com und sonst nirgendsGuardrails: Read-only → 403über das Schreiben wird in macOS entschieden,und Require approval stellt einen Dialog davor
Dasselbe Schreib-Zugangsdatum an zwei Orten. Auf einem Laptop ist es ein echtes Token in einer echten Datei, einen gewendeten Agenten entfernt von dem Artefakt, das deine Nutzer abrufen. In einem Bromure-Profil betritt das echte Token die VM nie, und über die Operation, die es ausführen würde, entscheidet der Host.

Der Laufzeit-Abruf kreuzt den Proxy ebenfalls

Nimm nun die stromabwärts gelegene Seite der Geschichte, in der du einer der 350.000 bist. Was du dort fangen kannst, hängt davon ab, wo du die Prüfung platzierst.

Bromures Supply Chain-Panel prüft Paketabrufe über npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go-Module und Packagist: eine standardmäßig aktive Altersschranke, die alles ablehnt, was in den letzten zwei Tagen veröffentlicht wurde, OSV-Abfragen, socket.dev oder Delpi als Filteranbieter, das Entfernen von Install-Skripten, das den Tarball umschreibt und den Hash in den Registry-Metadaten nachzieht, damit npm weiterhin verifiziert, und eine Rückfrage, bevor per Lockfile gepinnte Tarballs unverändert durchgehen. Die .npmrc und pip.conf in der VM können diese Regeln verschärfen und können sie nicht lockern.

Diese Regeln verdienen ihr Geld wegen des Ortes, an dem Bromure sie durchsetzt. Der Host-Proxy wendet sie auf eine Anfrage an statt auf ein Paket, sodass eine JSON-Datei, die zur Laufzeit von einem Build-Schritt, einer CLI, einem MCP-Server oder der Anwendung selbst aus einem Hersteller-Bucket gezogen wird, dieselbe Grenze kreuzt wie ein Tarball. Alles, was die VM zum Netz sagt, kreuzt sie.

Das verändert, wie die sieben Wochen aussehen. Steht Session trace auf Activity, protokolliert Bromure Host, Status, Latenz, den Tauschbericht und etwaige Leck-Warnungen für jede Anfrage der VM. Stell es auf Everything, und es behält auch die Bodies, für jeden Host, verschlüsselt mit demselben Keychain-Schlüssel wie deine Profil-Secrets und lesbar im Trace Inspector. Der Tag, an dem ein Feed, von dem dein Build abhängt, anders zu antworten beginnt, ist ein Diff, das du öffnen kannst, auf dem Mac, in einem Log, auf das die VM keinen Schreibzugriff hat.

Prüfung im Paketmanagersieht: Tarballs, Versionen, Lockfilesläuft, wenn du etwas installierstGET banner.json → der Hersteller-Bucketkeine Installation, also kein Ereignissieben Wochen, kein Eintrag behaltenPrüfung im Host-Proxysieht: jede Anfrage der VMAlter, OSV, Filter, Skript-EntfernungGET banner.json → der Hersteller-BucketHost, Status, Latenz, Tauschberichtund der Body, auf der Stufe Everything
Wo die Prüfung stattfindet, entscheidet darüber, was sie sehen kann. Eine Kontrolle im Paketmanager prüft nur Dinge, die veröffentlicht wurden; eine Kontrolle im Host-Proxy prüft jede Anfrage der VM, einschließlich des Laufzeit-Abrufs, der diese Payload trug.

Ein vergifteter Feed ist am Ende auch nur Text, der auf einer Maschine ankommt. Wenn er den Agenten als tool_result, als Datei, als abgerufene Seite oder als Anweisungsdatei wie CLAUDE.md oder AGENTS.md erreicht, bewertet ihn das Prompt Injection-Panel auf dem Gerät mit einem lokalen Modell, und nichts verlässt den Mac. Wähle Ask me what to do, und die Anfrage pausiert mit dem markierten Abschnitt vor Augen; wähle Block unilaterally, und der Agent bekommt ein hartes 451. Wenn die Maschine weg soll, setzt Erase home /home/ubuntu zurück und Reset to base klont die Systemplatte des Workspace neu aus dem Basis-Image.

BdThemes wird das Escaping reparieren, der Bucket bekommt engere Schlüssel, und die nächste Fassung dieser Geschichte kommt mit einem anderen Hersteller und einem anderen Feed. Was bleibt, ist die Anordnung darunter. Deine Entwicklungsumgebung hält eine Handvoll Zugangsdaten, von denen jedes etwas umschreiben kann, das Tausende von Menschen regelmäßig abrufen, und dieselbe Umgebung lässt jetzt einen Agenten laufen, der den ganzen Tag Text von Fremden liest.

Die meisten Beiträge hier fragen, was dein Agent lesen dürfen sollte. Beantworte auch die andere Frage, die auf einem Laptop unangenehm und in einem Profil leicht ist: Was darf er schreiben, und für wie viele Menschen?

Installiere Bromure Agentic Coding und gib dem Agenten eine Maschine, die keinen Schlüssel zu irgendetwas hat, das du ausliefert.