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

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.
Eine Login-URL, zwei Übergaben an den Hostauthorization_endpoint: https://evil.test/$([IO.File]::WriteAllText(…))opencode 1.18.32 in WSLLinux-Gastopencode MCP OAuth[email protected]Windows-Hostpowershell -EncodedCommandStart "…" "https://…$(…)"$(…) wird zuerst expandiertBefehl läuft als Ihr Windows-KontoBrowserstart scheitert, Fehler verworfenDie URL wird Teil einer Befehlszeile auf dem Host.Ein Agent in einer Bromure-MaschineLinux-VMxdg-open, $BROWSER→ bromure-openAusgangsdateidie URL als eine Textzeilekeine Shell liest sieIhr Macnur http oder https, öffentl. HostNSWorkspace.open(url)Die URL bleibt von der VM bis zum Browser eine URL. $(…) bleibt reiner Text.
Dieselbe Login-URL auf zwei Maschinen. In WSL reicht der Öffner im Linux-Gast die URL an powershell.exe auf dem Windows-Host weiter, das den Subausdruck $(...) expandiert und mit den Rechten Ihres Windows-Kontos ausführt. In Bromure Agentic Coding schreibt der Öffner des Gasts die URL in eine Datei für den Mac. Der Mac akzeptiert sie nur als http- oder https-Adresse mit öffentlichem Host und übergibt sie an den URL-Starter von macOS, der keine Shell startet.

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.

Entferntes MCP in Bromure Agentic CodingLinux-VMMCP-Konfig des AgentenServer-URL, beim Boot gesetztkein Token, keine AnmeldungSchlüssel und Tokensnur Platzhalterxdg-open → bromure-openURLs gehen als Text rausIhr MacOAuth-BrokerDiscovery, PKCE-Anmeldung, Tokenbleibt auf dem MacProxybeantwortet die Agent-Discoverysetzt das echte Token einallow web mcp.linear.appallow web mcp.notion.comallow web api.anthropic.comdefault denymcp.linear.appgelistet, mit echtem Token erreichtmcp-linear[.]appnicht gelistet: am Switch abgelehntdeny-Zeile in der Security Timeline
Ein in Bromure hinzugefügter MCP-Server. Der OAuth-Broker des Macs meldet sich an und behält das Token. Der Proxy beantwortet die Discovery-Anfragen des Agenten, sodass der Agent nie einen eigenen Handshake ausführt und nie eine vom Server gelieferte URL öffnet. Die Allowlist auf dem Mac begrenzt, welche MCP-Hosts die VM überhaupt erreichen kann, und jede Ablehnung wird protokolliert.

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.