← Volver a todas las publicaciones
Publicado el · por Renaud Deraison

El enlace de inicio de sesión era un comando

El 19 de octubre, Pillar Security reveló una falla de ejecución remota de código en opencode en Windows y WSL. Un servidor MCP remoto podía meter una subexpresión de PowerShell dentro de una URL de inicio de sesión https:// corriente, y el abridor de navegador de opencode la ejecutaba durante el handshake OAuth. La corrección salió como una refactorización, sin CVE y sin aviso de seguridad. En Bromure Agentic Coding, el propio Mac hace los inicios de sesión MCP, y una URL que viene de la VM llega al Mac como texto que ningún shell lee.

El servidor eligió la página de inicio de sesión, y la dirección de esa página llevaba un comando dentro. El agente la abrió, y PowerShell ejecutó el comando antes de que apareciera ningún navegador.

Añades un servidor MCP remoto a tu agente de programación. MCP, el Model Context Protocol, es la forma en que los agentes llegan a herramientas externas: un gestor de incidencias, una base de datos, un índice de documentación. El servidor te pide que inicies sesión, así que el agente ejecuta el handshake OAuth. Pregunta al servidor dónde está su página de inicio de sesión, recibe una dirección y la abre en tu navegador. Tú nunca tocas esa dirección. Ahorrarte ese copiar y pegar es justo el propósito del handshake.

El 19 de octubre, Pillar Security publicó Open and Shut: Remote Code Execution in opencode via MCP OAuth, de Eilon Cohen. opencode es un agente de programación de código abierto con unos 3 millones de descargas semanales en npm. En Windows y en WSL, las versiones 1.0.137 a 1.18.32 ejecutaban durante ese handshake un comando elegido por el servidor MCP, sin aviso y sin clic. Pillar propone una puntuación CVSS de 9,8. Infosecurity Magazine lo cubrió el 20 de octubre.

De un documento de descubrimiento a PowerShell

La dirección la controla el servidor. En el handshake OAuth, el documento de descubrimiento del servidor indica un authorization_endpoint, una cadena libre, y el cliente te envía allí. Pillar siguió su recorrido por opencode 1.18.32:

server's discovery doc → MCP SDK → oauth-provider.ts → mcp/index.ts → browser.ts → open() → PowerShell

Nada en ese recorrido comprobaba el esquema ni comparaba la dirección con el servidor que habías configurado. open es un paquete npm popular que abre un navegador, con unos 184 millones de descargas por semana. En macOS y Linux pasa la URL a open o xdg-open como un único argumento, así que ningún shell la lee. En Windows, y en WSL, donde recurre al host Windows, la versión 10.1.2 construye una línea de comandos de PowerShell. Escapa el nombre de la aplicación y deja la URL tal cual, entre comillas dobles. PowerShell expande $(...) dentro de las cadenas entre comillas dobles. La carga útil de Pillar:

https://evil.test/$([IO.File]::WriteAllText('C:\Users\Public\pwned.txt','RCE'))

PowerShell evalúa la subexpresión antes de intentar lanzar nada. Cuando el lanzamiento del navegador falla, el archivo ya está en el disco, y opencode descarta el error.

La URL empieza por https://. Tras un bug idéntico en mcp-remote (CVE-2025-6514, julio de 2025), el consejo habitual era aceptar solo enlaces http: y https:. El SDK de TypeScript de MCP bloquea javascript:, data: y vbscript:. El propio parche de opencode añadió una comprobación de http/https. La carga útil de Pillar supera las tres. Lo que cerró el agujero en opencode 1.18.33 fue la actualización a open 11.0.4, que envuelve la URL en comillas simples, donde PowerShell no expande nada.

Dos detalles más hacen que este caso sea difícil de gestionar:

  • El servidor puede volverse hostil después de que lo añadas. La carga útil llega en una respuesta HTTP en el momento de la conexión. Un servidor que estaba bien el día que lo configuraste puede verse comprometido más tarde, igual que su dominio, su entrada en un registro o una copia con typosquatting.
  • Ningún escáner lo señalará. Pillar informó del bug mediante un aviso privado de GitHub el 29 de agosto. Anomaly, la desarrolladora de opencode, dijo que los servidores MCP maliciosos quedaban fuera de su modelo de amenazas, fusionó la corrección el 25 de septiembre como una refactorización del código que lanza el navegador y publicó la 1.18.33 el 28 de septiembre. No pidió CVE ni publicó aviso, así que los bots de dependencias no tienen nada con qué comparar. Pillar aconseja inventariar a mano las instalaciones de opencode.
Una URL de inicio de sesión, dos traspasos al hostauthorization_endpoint: https://evil.test/$([IO.File]::WriteAllText(…))opencode 1.18.32 en WSLInvitado Linuxopencode MCP OAuth[email protected]Host Windowspowershell -EncodedCommandStart "…" "https://…$(…)"$(…) se expande primeroel comando corre como tu usuarioel navegador falla, error descartadoLa URL pasa a formar parte de una línea de comandos en el host.Un agente en una máquina BromureVM Linuxxdg-open, $BROWSER→ bromure-openArchivo de salidala URL como una línea de textoningún shell la leeTu Macsolo http o https, host públicoNSWorkspace.open(url)La URL sigue siendo una URL de la VM al navegador. $(…) sigue siendo texto plano.
La misma URL de inicio de sesión en dos máquinas. En WSL, el abridor dentro del invitado Linux entrega la URL a powershell.exe en el host Windows, que expande la subexpresión $(...) y la ejecuta con los privilegios de tu cuenta de Windows. En Bromure Agentic Coding, el abridor del invitado escribe la URL en un archivo para el Mac. El Mac solo la acepta como dirección http o https con host público y se la pasa al lanzador de URL del propio macOS, que no arranca ningún shell.

WSL y Bromure, a una decisión de diseño de distancia

WSL convierte este bug en un compromiso del host. Ejecuta Linux sobre Windows, y cuando un programa de dentro quiere un navegador, la petición va al host. Un shell de Windows se encarga de abrirlo, así que un comando escondido en la URL se ejecuta en tu máquina Windows, con tu cuenta de Windows, fuera de Linux.

Bromure Agentic Coding tiene la misma disposición. Ejecuta Claude Code, Codex, Grok, Kimi y omp en VM Linux en tu Mac, y una página de inicio de sesión abierta desde la VM aparece en el navegador de tu Mac. Los dos se separan en el traspaso.

En cada máquina Bromure, xdg-open y $BROWSER apuntan a un pequeño script llamado bromure-open. El paquete open en Linux llama a xdg-open, igual que la mayoría de las CLI que “abren un navegador”. bromure-open hace una sola cosa: escribe la URL, como una única línea de texto, en un archivo de una carpeta compartida con el Mac. No ejecuta ni expande nada de ella.

El Mac recoge el archivo, lo borra y lee la línea como una URL. Solo pasan las direcciones http y https con host público. mailto:, file:, los esquemas personalizados, localhost y las direcciones privadas se descartan. Lo que queda va a NSWorkspace.open, la llamada de macOS que entrega una URL a tu navegador predeterminado. No interviene ningún shell, a ningún lado de la frontera de la VM.

La URL de Pillar llega a tu navegador como URL, con $([IO.File]::WriteAllText(…)) en su ruta como texto plano. El navegador abre una página en evil.test, o no lo consigue, y no se ejecuta ningún comando.

El Mac hace el inicio de sesión MCP

Con los agentes que incluye Bromure, la URL de inicio de sesión ni siquiera llega a la VM. Añades los servidores MCP remotos una vez, en el panel MCP de la máquina. Bromure traduce cada uno al formato de configuración de Claude Code, Codex, Grok, Kimi y omp y se lo entrega a la VM al arrancar.

Para un servidor HTTP, Authorize with OAuth… hace el inicio de sesión en el Mac. El intermediario de Bromure obtiene el documento de descubrimiento del servidor, se registra como Bromure AC, construye la URL de autorización con las API de URL de Apple y la abre con NSWorkspace.open, y luego recoge el callback en un puerto de loopback del Mac. El token se queda en el Mac. La configuración del agente no lleva ninguna credencial, y el proxy añade la cabecera de autorización a las peticiones dirigidas a ese servidor.

El proxy también responde él mismo a las peticiones de descubrimiento OAuth del servidor. El agente ve un servidor en el que ya ha iniciado sesión, así que nunca empieza un handshake propio, nunca pide al servidor un authorization_endpoint y nunca le pasa uno a un abridor. La ruta de código que explotó Pillar no se ejecuta.

La VM no decide adónde se conecta

Pillar advierte de que un servidor puede volverse hostil después de que lo añadas. Bromure controla desde el Mac qué servidores puede alcanzar la VM. Cada conexión que hace una máquina pasa por un switch de red virtual y un proxy, ambos fuera de la VM. En el panel Guardrails de la máquina, pon Unmatched traffic en Deny y enumera los servidores MCP que usas:

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

Un servidor que no está en la lista, como un typosquat o una entrada que alguien pegó desde un foro, no puede servir un documento de descubrimiento, así que no hay authorization_endpoint que abrir. El switch comprueba cada conexión por IP y por el nombre de host que vio en el DNS, y el proxy la vuelve a comprobar por el nombre de servidor TLS. Cada rechazo es una fila roja deny en la Security Timeline (Window → Security Timeline…) con el host, el puerto y la máquina. Las reglas se aplican en cuanto haces clic en Save, y nada dentro de la VM puede editarlas.

Dentro de la VM, cada clave es un sustituto. ANTHROPIC_API_KEY, el token de gh, el secreto de AWS y los tokens estáticos de MCP guardan valores falsos, y el proxy pone el verdadero solo en una petición al host para el que se emitió. Si un comando recoge esos valores y envía uno a un host al que no pertenece, el detector de compromiso responde HTTP 451, no reenvía nada y pausa la máquina con This environment may have been compromised.

MCP remoto en Bromure Agentic CodingVM LinuxConfig MCP del agenteURL del servidor, al arrancarsin token, sin login que hacerClaves y tokenssolo sustitutosxdg-open → bromure-openlas URL salen como textoTu MacIntermediario OAuthdescubrimiento, login PKCE, tokenguardado en el MacProxyresponde al descubrimientopone el token real en la conexiónallow web mcp.linear.appallow web mcp.notion.comallow web api.anthropic.comdefault denymcp.linear.appen la lista, con el token realmcp-linear[.]appfuera de la lista: rechazadofila deny en la Security Timeline
Un servidor MCP añadido en Bromure. El intermediario OAuth del Mac inicia sesión y guarda el token. El proxy responde a las peticiones de descubrimiento del agente, así que el agente nunca ejecuta su propio handshake ni abre una URL proporcionada por el servidor. La lista de permitidos en el Mac limita qué hosts MCP puede siquiera alcanzar la VM, y cada rechazo queda registrado.

Encontrar la versión que ningún escáner señala

Sin CVE, el consejo de Pillar es inventariar tú mismo las instalaciones de opencode. En Bromure, los registros ya existen.

Encuentra la versión. La Security Timeline registra cada descarga de paquetes, sean cuales sean los ajustes de cadena de suministro, con el paquete y la versión en la columna Condition. Escribe opencode-ai en Filter events…, y cualquier fila que diga npm · [email protected] o anterior señala la máquina. Export… guarda las filas como CSV. En los Mac inscritos en bromure.io, cada descarga también es un evento supply_chain.fetch, así que un administrador puede buscar opencode-ai en las máquinas de todos los desarrolladores desde una sola consola, en lugar de preguntar a cada equipo.

Encuentra los servidores. bromure-cli trace hostnames lista cada host que una máquina contactó, con el número de peticiones. Los hosts MCP con los que hablan tus agentes están en esa lista, incluidos los que nunca pasaron por el panel MCP. Esos son los que hay que revisar, o dejar fuera de la lista de permitidos.

Actualiza en la VM. npm install -g opencode-ai@latest en la terminal de la máquina instala la 1.18.33 o posterior, y la nueva descarga aparece en la Timeline como prueba.

Las URL salen de la VM como texto

xdg-open y $BROWSER escriben la URL en un archivo para el Mac. El Mac solo acepta http o https con host público y la abre sin shell.

Inicio de sesión MCP en el Mac

El intermediario ejecuta OAuth y guarda el token. El proxy responde al descubrimiento del agente, así que el agente nunca abre una URL de inicio de sesión proporcionada por el servidor.

Una lista de permitidos para hosts MCP

Con Unmatched traffic en Deny, solo se puede llegar a los servidores que enumeras. Cada rechazo es una fila en la Security Timeline.

Un inventario sin CVE

Cada descarga de paquetes queda registrada con su versión, y cada host que contactó una máquina está a un comando de distancia.

Todo campo es no confiable

La lección final de Pillar es para quienes construyen clientes MCP: todo campo que envía un servidor es una entrada no confiable, y la mayor parte se analiza mucho antes de que el modelo lea nada. En este bug el modelo nunca vio la carga útil: la ejecutó un lanzador de navegador.

No puedes auditar cada lanzador de cada agente que instalas, y este se corrigió sin avisar a nadie. Bromure saca las decisiones de la VM. El Mac hace los inicios de sesión, abre las URL y decide qué hosts puede alcanzar la VM. El Mac también guarda las credenciales reales. Para un caso anterior en el que el propio servidor MCP era el atacante, lee El modelo era su servidor. Para una configuración de agente que confiaba en la fuente equivocada, lee Confiaste en el espacio de trabajo, no en los servidores.