El notebook traía su propio servidor MCP
CVE-2026-75149 es un fallo de inyección de código en marimo: un notebook es un único archivo Python, ese archivo lleva su propia configuración en un bloque de comentarios y, hasta la versión 0.23.15, esa configuración podía declarar un servidor MCP. Bastaba con abrir el notebook en modo edición para que marimo lanzara el comando del atacante como subproceso, antes de que se ejecutara una sola celda. El parche dice el resto. Elimina cinco categorías enteras de entorno que un documento descargado tenía permiso para fijar, con más prioridad que la configuración del operador. En Bromure Agentic Coding la lista de programas que tu agente puede lanzar es un panel del lado del host, escrito en la VM al arrancar, y nada dentro del espacio de trabajo puede añadir nada a ella.
Un repositorio puede reconfigurar tu agente de codificación. Eso ya lo sabíamos. Lo que ha cambiado es el tamaño de la cosa capaz de hacerlo. Ahora es un solo archivo, reenviado por Slack, que abres para echarle un vistazo.
Alguien te envía un notebook. Es un notebook de marimo, así que es un archivo
.py corriente y no un paquete de JSON con salidas en base64 dentro, que es
casi toda la razón por la que a la gente le gusta el formato. Puedes leerlo,
puedes hacerle diff y puedes meterlo en git sin un hook de pre-commit que
elimine las salidas.
Así que lo lees, tiene buena pinta, y lo abres en modo edición para toquetear una celda.
Arranca un subproceso. Todavía no has ejecutado ninguna celda; esto es algo que el archivo pidió por su cuenta, antes de que el notebook terminara de cargar.
Eso es CVE-2026-75149, publicada el 19 de agosto y recogida esta semana por The Hacker News. CVSS 8.7, CWE-94, atribuida a Gregory Tan, corregida en marimo 0.23.15. El aviso de VulnCheck resume el mecanismo en una línea: el notebook contiene una entrada de servidor MCP manipulada, y abrirlo en modo edición lanza el comando de esa entrada como subproceso local.
La configuración estaba en un comentario
Los notebooks de marimo llevan sus propios ajustes mediante
PEP 723, la convención de Python para
metadatos de script en línea. Es una pequeña tabla TOML que vive dentro de un
bloque de comentarios al principio del archivo, y existe para que un script
pueda declarar sus dependencias y su versión de Python sin un archivo de
proyecto al lado. uv la lee. marimo también, y la extiende con una tabla
tool.marimo para la configuración del notebook.
Dos propiedades de esa tabla hicieron el daño, y ninguna de las dos es un fallo por sí sola.
La primera: marimo fusionaba los metadatos del notebook con la máxima prioridad, por encima de la configuración que hubiera fijado la persona que ejecuta marimo. Ese es el comportamiento correcto por defecto para una preferencia de formato. Un notebook que quiere una indentación de dos espacios debería ganarle a tu ajuste global, porque el autor sabe qué aspecto tiene su archivo y tú no.
La segunda: la fusión era casi sin filtrar. Antes del arreglo, el saneador de
marimo eliminaba exactamente una clave de la configuración suministrada por el
notebook: tool.marimo.runtime.auto_instantiate. Todo lo demás de tool.marimo
pasaba, incluido tool.marimo.mcp. Ahí es donde se listan los servidores Model
Context Protocol, y una lista de servidores MCP es una lista de comandos que
lanzar.
Junta las dos cosas y un comentario al principio de un archivo descargado podía nombrar un programa y hacer que marimo lo arrancara. Nada de esto es un exploit. El archivo usó la funcionalidad tal y como fue diseñada, y el diseño daba por supuesto que el archivo lo habías escrito tú.
El parche es la divulgación
El arreglo, PR #10281, se titula “additional pep 723 sanitization”, y cambia una lista de denegación por una lista de permitidos. La configuración suministrada por el notebook queda ahora restringida a secciones cosméticas y de editor: formato, guardado, visualización, atajos de teclado, diagnósticos, lint, snippets, fuentes de datos, servidores de lenguaje, compartición, venv, tiempo de ejecución y gestión de paquetes.
Las secciones eliminadas son la mitad interesante: ai, mcp, completion,
secrets, server.
Son cinco secciones enteras, no cinco claves, y un documento que te habías
descargado podía fijarlas todas. La justificación del PR detalla además una
segunda carga útil que nunca necesitó un subproceso. La sección ai contiene
URL base de API, así que un notebook podía reapuntar el endpoint del modelo a un
host de su elección, lo que, en palabras de los mantenedores, “podría permitir
la exfiltración de las claves de API del operador hacia endpoints controlados
por el atacante”.
Cubrimos la variante de URL base de este ataque en julio, cuando
un repositorio podía fijar ANTHROPIC_BASE_URL
y Claude Code mandaba su propia clave a la dirección que el repositorio
eligiera. Es el ataque más barato de esta categoría, porque no se ejecuta código
y es el propio cliente de la víctima el que hace el envío. La variante de marimo
encoge el vehículo de entrega de un repositorio a un comentario.
La unidad de confianza no para de encoger
Esta clase de fallos tiene una historia corta, y en esa historia el atacante necesita cada vez menos de tu máquina.
En abril, Wiz Research informó de
CVE-2026-12957
en Amazon Q Developer, CVSS 8.5, parcheada en mayo y divulgada en junio. Amazon
Q leía .amazonq/mcp.json de un espacio de trabajo abierto y lanzaba los
servidores que definía. Esos procesos heredaban todo el entorno del
desarrollador: claves de AWS, tokens de CLI en la nube, secretos de API, sockets
del agente SSH. El arreglo de Amazon fue preguntar antes de ejecutar un servidor
MCP desde un espacio de trabajo no confiable.
En agosto, ChainDrop
metía configuración de agente en los repositorios que recorría:
un hook SessionStart en .claude/settings.json y una tarea folderOpen en
.vscode/tasks.json, de modo que clonar el proyecto y abrirlo bastaba.
Los dos necesitaban un repositorio. Alguien tenía que publicar o comprometer un proyecto, tú tenías que clonarlo, y un directorio de dotfiles se quedaba ahí, en el árbol, donde una persona desconfiada podía mirarlo.
Marimo necesita un archivo. Un archivo, que se muestra como un documento y que llega como llegan los documentos: un hilo de Slack, un adjunto de correo, un gist que alguien enlazó en la reunión diaria, una descarga de Kaggle. Un notebook no se clona. Se abre, porque abrirlo es la manera de averiguar qué es.
El bloque mcp se ha extendido a casi todo el instrumental de desarrollo en los
últimos dieciocho meses, y esos bloques viven en archivos que viajan entre
personas: raíces de repositorio, directorios de editor y ahora metadatos de
documento. Cada uno es una lista de programas que algo de tu máquina está
dispuesto a arrancar, escrita por quienquiera que tocara el archivo el último.
Los escáneres de registro no los leen, los lockfiles no los cubren, y nadie ha
propuesto una atestación de procedencia que se aplique a un comentario.
De dónde salen los servidores MCP en un perfil
Bromure Agentic Coding responde a la pregunta de la procedencia donde se puede responder: en el host, antes de que arranque la VM.
Cada perfil tiene un panel MCP en sus ajustes, junto a Agents, Credenciales, Guardrails y Supply Chain. Contiene una lista de servidores. Cada entrada se activa o desactiva de forma independiente y usa uno de dos transportes: HTTP, una URL remota con un token bearer opcional, o stdio, un comando local lanzado dentro de la VM. Bromure traduce esa lista al formato que espera el agente activo, JSON para Claude Code y TOML para Codex, y la inyecta en la VM al arrancar.
Lee la dirección del viaje en esa última frase. La configuración va hacia dentro del invitado, desde el host, al arrancar, a partir de una lista que mantienes en un panel de ajustes. Un archivo que aparece después en el espacio de trabajo no es un sitio del que salgan servidores MCP, así que no hay ninguna fusión cuya prioridad se pueda equivocar ni ningún saneador que mantener sincronizado con un esquema de configuración al que no paran de salirle secciones. Un documento no puede añadir una entrada a una lista que no puede alcanzar.
Ahora abre el notebook igualmente
Nada de eso ayuda el día en que estás ejecutando una versión que tiene el fallo. Así que coge el marimo anterior a 0.23.15, regálale la victoria al atacante y haz pasar un notebook hostil por un perfil.
El subproceso arranca dentro del ordenador de otro. El trabajo de sesión en
Bromure Agentic Coding ocurre en pestañas de kitty dentro de la VM Ubuntu del
perfil, a un hipervisor de distancia de macOS. marimo edit se ejecuta ahí, y
el comando del comentario también. En Recursos → Almacenamiento el perfil
son tres capas: un /home/ubuntu por perfil con Borrar home…, un disco de
sistema del espacio de trabajo con Restablecer a la base… que vuelve a clonar
desde la imagen compartida, y debajo un sistema operativo base de solo lectura
que nada del invitado puede tocar. Una opción de menú se lleva por delante todo
lo que la entrada instalara.
El entorno que hereda es un conjunto de señuelos. El fallo de Amazon Q dolía
porque el servidor lanzado era hijo de la shell real del desarrollador y
heredaba sus credenciales reales. En un perfil de Bromure no hay nada detrás de
esos nombres de variable. Las claves de API genéricas son marcadores de posición
brm_… exportados a la VM y cambiados por los valores reales por el proxy del
host de camino hacia fuera. El kubeconfig es sintético, con certificados de
cliente de usar y tirar. Las peticiones a AWS se vuelven a firmar en el host, de
modo que un proceso que esquive el proxy recibe InvalidSignatureException en
lugar de acceso. ~/.docker/config.json contiene un blob base64 falso. Las
claves SSH privadas
nunca están en la VM, porque el host
firma a través de un agente por perfil. Activa Requerir aprobación para usar
en una credencial concreta y cada cambio se convierte en un diálogo en el host
con una concesión acotada en el tiempo: cinco minutos, una hora, el resto de la
sesión.
El truco de la URL base recoge un marcador de posición. El proxy sustituye
una credencial real solo en peticiones salientes hacia el host propio de esa
credencial. Un notebook que reapunte un endpoint de IA a attacker.example
recoge lo que hay en el entorno de la VM, que es una cadena brm_… que no abre
nada.
El servidor tiene que llegar a la red. Una entrada MCP que lance algo útil
para un atacante necesita salida, ya sea para traerse una fase posterior o para
devolver lo que encontró. Guardrails → Conexiones salientes es una tabla de
reglas al estilo pf: una acción, un protocolo (tcp, udp, web, any), un
host o CIDR, una lista de puertos y, para web, una lista de métodos HTTP,
evaluadas de arriba abajo y ganando la primera coincidencia, más un ajuste
Tráfico no coincidente en Permitir o Denegar. Ponlo en Denegar, lista los
hosts que tu trabajo necesita, y el destino de la entrada no está en la lista.
La tabla se aplica en el host, en el conmutador virtual y en el proxy, así que
nada de lo que el invitado le haga a su propio enrutado cambia la respuesta.
El archivo tiene que estar dentro de la VM, para empezar. Un perfil comparte
como mucho ocho carpetas del Mac, cada una elegida a mano en el panel
Carpetas y montada en /home/ubuntu/<basename>. Un notebook que aterrizó en
~/Downloads no está en el perfil hasta que lo pongas ahí, y el resto de tu Mac
no está a un recorrido de directorios de lo que el notebook arrancara.
Y puedes verlo ocurrir. La ventana Registro de seguridad (Ventana → Registro de seguridad…) es una única tabla cronológica del lado del host: veredictos de paquetes, decisiones del cortafuegos, cambios de credenciales, detecciones de inyección de prompts. El código invitado no puede editarla, porque el código invitado no puede alcanzarla. Una conexión saliente bloqueada desde un proceso que tú no arrancaste aparece ahí como una línea, lo que es mejor que enterarte por una factura de nube seis semanas después.
Actívalo
Actualiza marimo a 0.23.15 o posterior; la lista de permitidos que hay ahí es un
buen parche. Después ve a mirar cualquier otro sitio del que tu instrumental lea
un bloque mcp, y pregunta quién puede escribir ese archivo.
En un perfil, los ajustes que valen dos minutos son la misma lista corta de siempre. Guardrails → Conexiones salientes, con Tráfico no coincidente en Denegar y una lista de permitidos para los hosts que tu trabajo necesita. Credenciales → Requerir aprobación para usar en todo lo que pueda gastar dinero o borrar datos. Supply Chain → Comprobación de vulnerabilidades OSV, y el filtrado de socket.dev o Delpi si tienes clave, encima de la puerta de edad de dos días activa por defecto. Prompt Injection → el escáner de CLAUDE.md y AGENTS.md, que trata a los archivos que se arrogan autoridad sobre tu agente exactamente como lo que son.
Los notebooks van a seguir reenviándose, y a los formatos de configuración les van a seguir saliendo secciones, porque las dos cosas son útiles. Lo que sí puedes cambiar es qué máquina está escuchando cuando un documento pide que se arranque un programa. Instala Bromure Agentic Coding, guarda tu lista de servidores en el editor de perfiles, y deja que el próximo bloque de comentarios servicial configure una VM que puedes borrar desde un menú.