Der Login-Link war ein Befehl
Am 19. Oktober hat Pillar Security eine Remote-Code-Execution-Lücke in opencode unter Windows und WSL offengelegt. Ein entfernter MCP-Server konnte einen PowerShell-Subausdruck in eine gewöhnliche https://-Login-URL packen, und der Browser-Öffner von opencode führte ihn während des OAuth-Handshakes aus. Der Fix kam als Refactoring, ohne CVE und ohne Advisory. In Bromure Agentic Coding erledigt der Mac MCP-Anmeldungen selbst, und eine URL aus der VM erreicht den Mac als Text, den nie eine Shell liest.
Der Server hat die Login-Seite ausgesucht, und in der Adresse dieser Seite steckte ein Befehl. Der Agent hat sie geöffnet, und PowerShell hat den Befehl ausgeführt, bevor überhaupt ein Browser erschien.
Sie fügen Ihrem Coding-Agenten einen entfernten MCP-Server hinzu. MCP, das Model Context Protocol, ist der Weg, über den Agenten externe Werkzeuge erreichen: einen Issue-Tracker, eine Datenbank, einen Dokumentationsindex. Der Server bittet Sie, sich anzumelden, also führt der Agent den OAuth-Handshake aus. Er fragt den Server, wo dessen Login-Seite liegt, bekommt eine Adresse zurück und öffnet sie in Ihrem Browser. Sie fassen die Adresse nie an. Ihnen dieses Kopieren und Einfügen zu ersparen, ist der Sinn des Handshakes.
Am 19. Oktober hat Pillar Security Open and Shut: Remote Code Execution in opencode via MCP OAuth von Eilon Cohen veröffentlicht. opencode ist ein Open-Source-Coding-Agent mit rund 3 Millionen npm-Downloads pro Woche. Unter Windows und in WSL führten die Versionen 1.0.137 bis 1.18.32 während dieses Handshakes einen vom MCP-Server gewählten Befehl aus, ohne Rückfrage und ohne Klick. Pillar schlägt einen CVSS-Wert von 9,8 vor. Infosecurity Magazine berichtete am 20. Oktober darüber.
Vom Discovery-Dokument zu PowerShell
Der Server bestimmt die Adresse. Im OAuth-Handshake nennt das
Discovery-Dokument des Servers einen authorization_endpoint, einen
beliebigen String, und der Client schickt Sie dorthin. Pillar hat seinen
Weg durch opencode 1.18.32 verfolgt:
server's discovery doc → MCP SDK → oauth-provider.ts → mcp/index.ts → browser.ts → open() → PowerShell
Nichts auf diesem Weg prüfte das Schema oder verglich die Adresse mit dem
Server, den Sie konfiguriert hatten. open ist ein beliebtes npm-Paket,
das einen Browser startet, mit rund 184 Millionen Downloads pro Woche.
Unter macOS und Linux übergibt es die URL als einzelnes Argument an open
oder xdg-open, sodass keine Shell sie liest. Unter Windows, und in WSL,
wo es auf den Windows-Host zugreift, baut Version 10.1.2 eine
PowerShell-Befehlszeile. Sie maskiert den Anwendungsnamen und lässt die
URL, wie sie ist, in doppelten Anführungszeichen. PowerShell expandiert
$(...) in Strings mit doppelten Anführungszeichen. Pillars Payload:
https://evil.test/$([IO.File]::WriteAllText('C:\Users\Public\pwned.txt','RCE'))
PowerShell wertet den Subausdruck aus, bevor es versucht, irgendetwas zu starten. Wenn der Browserstart scheitert, liegt die Datei schon auf der Platte, und opencode verwirft den Fehler.
Die URL beginnt mit https://. Nach einem identischen Bug in mcp-remote
(CVE-2025-6514, Juli
2025) lautete der übliche Rat, nur http:- und https:-Links zu
akzeptieren. Das TypeScript-SDK von MCP blockiert javascript:, data:
und vbscript:. opencodes eigener Patch fügte eine http/https-Prüfung
hinzu. Pillars Payload kommt an allen dreien vorbei. Geschlossen hat die
Lücke in opencode 1.18.33 das Upgrade auf open 11.0.4, das die URL in
einfache Anführungszeichen setzt, in denen PowerShell nichts expandiert.
Zwei weitere Details machen diesen Fall schwer beherrschbar:
- Der Server kann feindlich werden, nachdem Sie ihn hinzugefügt haben. Der Payload kommt in einer HTTP-Antwort beim Verbindungsaufbau. Ein Server, der an dem Tag, an dem Sie ihn konfiguriert haben, harmlos war, kann später kompromittiert werden, ebenso seine Domain, sein Registry-Eintrag oder eine typosquattete Kopie davon.
- Kein Scanner wird ihn melden. Pillar meldete den Bug am 29. August über ein privates GitHub-Advisory. Anomaly, der Entwickler von opencode, erklärte bösartige MCP-Server für außerhalb seines Bedrohungsmodells, mergte den Fix am 25. September als Refactoring des Browserstart-Codes und veröffentlichte 1.18.33 am 28. September. Er beantragte keine CVE und veröffentlichte kein Advisory, Dependency-Bots haben also nichts zum Abgleichen. Pillar rät, opencode-Installationen von Hand zu inventarisieren.
WSL und Bromure, eine Designentscheidung auseinander
WSL macht aus diesem Bug eine Kompromittierung des Hosts. Es lässt Linux auf Windows laufen, und wenn ein Programm darin einen Browser will, geht die Anfrage an den Host. Eine Windows-Shell übernimmt das Öffnen, also läuft ein in der URL versteckter Befehl auf Ihrem Windows-Rechner, mit Ihrem Windows-Konto, außerhalb von Linux.
Bromure Agentic Coding hat denselben Aufbau. Es betreibt Claude Code, Codex, Grok, Kimi und omp in Linux-VMs auf Ihrem Mac, und eine Login-Seite aus der VM öffnet sich im Browser Ihres Macs. Die beiden trennen sich bei der Übergabe.
In jeder Bromure-Maschine zeigen xdg-open und $BROWSER auf ein kleines
Skript namens bromure-open. Das open-Paket ruft unter Linux xdg-open
auf, wie die meisten CLIs, die „einen Browser öffnen“. bromure-open tut
genau eins: Es schreibt die URL als eine einzige Textzeile in eine Datei in
einem Ordner, den die VM mit dem Mac teilt. Es führt darin nichts aus und
expandiert nichts.
Der Mac holt die Datei ab, löscht sie und liest die Zeile als URL. Nur
http- und https-Adressen mit öffentlichem Host kommen durch.
mailto:, file:, eigene Schemata, localhost und private Adressen
werden verworfen. Was übrig bleibt, geht an NSWorkspace.open, den
macOS-Aufruf, der eine URL an Ihren Standardbrowser übergibt. Keine Shell
ist beteiligt, auf keiner Seite der VM-Grenze.
Pillars URL erreicht Ihren Browser als URL, mit
$([IO.File]::WriteAllText(…)) als bloßem Text im Pfad. Der Browser
öffnet eine Seite auf evil.test, oder scheitert daran, und kein Befehl
läuft.
Der Mac erledigt die MCP-Anmeldung
Mit den Agenten, die Bromure mitliefert, erreicht die Login-URL die VM gar nicht erst. Sie fügen entfernte MCP-Server einmal hinzu, im MCP-Bereich der Maschine. Bromure übersetzt jeden in das Konfigurationsformat von Claude Code, Codex, Grok, Kimi und omp und übergibt ihn der VM beim Booten.
Bei einem HTTP-Server führt Authorize with OAuth… die Anmeldung auf
dem Mac aus. Der Broker von Bromure holt das Discovery-Dokument des
Servers, registriert sich als Bromure AC, baut die Autorisierungs-URL
mit Apples URL-APIs und öffnet sie mit NSWorkspace.open, dann fängt er
den Callback auf einem Loopback-Port des Macs ab. Das Token bleibt auf dem
Mac. Die Konfiguration des Agenten enthält keine Zugangsdaten, und der
Proxy fügt den Authorization-Header zu Anfragen an diesen Server hinzu.
Der Proxy beantwortet auch die OAuth-Discovery-Anfragen an den Server
selbst. Der Agent sieht einen Server, bei dem er schon angemeldet ist,
startet also nie einen eigenen Handshake, fragt den Server nie nach einem
authorization_endpoint und gibt nie einen an einen Öffner weiter. Der
Codepfad, den Pillar ausgenutzt hat, läuft gar nicht.
Die VM entscheidet nicht, wohin sie sich verbindet
Pillar warnt, dass ein Server feindlich werden kann, nachdem Sie ihn hinzugefügt haben. Bromure legt vom Mac aus fest, welche Server die VM erreichen kann. Jede Verbindung einer Maschine läuft über einen virtuellen Netzwerk-Switch und einen Proxy, beide außerhalb der VM. Stellen Sie im Guardrails-Bereich der Maschine Unmatched traffic auf Deny und listen Sie die MCP-Server auf, die Sie nutzen:
allow web mcp.linear.app
allow web mcp.notion.com
allow web registry.npmjs.org
allow web github.com
allow web api.anthropic.com
default deny
Ein Server, der nicht auf der Liste steht, etwa ein Typosquat oder ein aus
einem Forum kopierter Eintrag, kann kein Discovery-Dokument ausliefern,
also gibt es keinen authorization_endpoint zum Öffnen. Der Switch prüft
jede Verbindung per IP und per Hostname, den er im DNS gesehen hat, und
der Proxy prüft sie erneut per TLS-Servername. Jede Ablehnung ist eine
rote deny-Zeile in der Security Timeline (Window → Security
Timeline…) mit Host, Port und Maschine. Die Regeln gelten, sobald Sie
auf Save klicken, und nichts in der VM kann sie ändern.
In der VM ist jeder Schlüssel ein Platzhalter. ANTHROPIC_API_KEY, das
gh-Token, das AWS-Secret und statische MCP-Tokens enthalten falsche
Werte, und der Proxy setzt den echten nur bei einer Anfrage an den Host
ein, für den er ausgestellt wurde. Sammelt ein Befehl diese Werte ein und
schickt einen davon an einen Host, zu dem er nicht gehört, antwortet der
Kompromittierungsdetektor mit HTTP 451, leitet nichts weiter und hält die
Maschine mit This environment may have been compromised an.
Die Version finden, die kein Scanner meldet
Ohne CVE lautet Pillars Rat, opencode-Installationen selbst zu inventarisieren. In Bromure gibt es die Aufzeichnungen schon.
Die Version finden. Die Security Timeline zeichnet jeden Paketabruf
auf, unabhängig von den Supply-Chain-Einstellungen, mit Paket und Version
in der Spalte Condition. Geben Sie opencode-ai in Filter events…
ein, und jede Zeile mit npm · [email protected] oder älter nennt die
Maschine. Export… speichert die Zeilen als CSV. Auf Macs, die bei
bromure.io registriert sind, ist jeder Abruf zusätzlich ein
supply_chain.fetch-Ereignis, sodass ein Administrator die Maschinen
aller Entwickler von einer Konsole aus nach opencode-ai durchsuchen
kann, statt jedes Team einzeln zu fragen.
Die Server finden. bromure-cli trace hostnames listet jeden Host, den
eine Maschine kontaktiert hat, mit der Zahl der Anfragen. Die MCP-Hosts,
mit denen Ihre Agenten sprechen, stehen in dieser Liste, auch solche, die
nie im MCP-Bereich gelandet sind. Genau die sollten Sie prüfen oder von
der Allowlist fernhalten.
In der VM aktualisieren. npm install -g opencode-ai@latest im
Terminal der Maschine installiert 1.18.33 oder neuer, und der neue Abruf
erscheint als Beleg in der Timeline.
URLs verlassen die VM als Text
xdg-open und $BROWSER schreiben die URL in eine Datei für den Mac.
Der Mac akzeptiert nur http oder https mit öffentlichem Host und öffnet
sie ohne Shell.
MCP-Anmeldung auf dem Mac
Der Broker führt OAuth aus und behält das Token. Der Proxy beantwortet die Discovery des Agenten, sodass der Agent nie eine vom Server gelieferte Login-URL öffnet.
Eine Allowlist für MCP-Hosts
Mit Unmatched traffic auf Deny sind nur die Server erreichbar, die Sie auflisten. Jede Ablehnung ist eine Zeile in der Security Timeline.
Ein Inventar ohne CVE
Jeder Paketabruf wird mit seiner Version aufgezeichnet, und jeder Host, den eine Maschine kontaktiert hat, ist einen Befehl entfernt.
Jedes Feld ist nicht vertrauenswürdig
Pillars abschließende Lehre richtet sich an alle, die MCP-Clients bauen: Jedes Feld, das ein Server schickt, ist nicht vertrauenswürdige Eingabe, und das meiste davon wird geparst, lange bevor das Modell irgendetwas liest. Bei diesem Bug hat das Modell den Payload nie gesehen: Ein Browser-Starter hat ihn ausgeführt.
Sie können nicht jeden Starter in jedem Agenten prüfen, den Sie installieren, und dieser hier wurde behoben, ohne dass es jemandem gesagt wurde. Bromure holt die Entscheidungen aus der VM heraus. Der Mac erledigt die Anmeldungen, öffnet die URLs und entscheidet, welche Hosts die VM erreichen kann. Auch die echten Zugangsdaten liegen auf dem Mac. Für einen früheren Fall, in dem der MCP-Server selbst der Angreifer war, lesen Sie Das Modell war ihr Server. Für eine Agentenkonfiguration, die der falschen Quelle vertraute, lesen Sie Du hast dem Workspace vertraut, nicht den Servern.