
# Laboratorio Docker local que reproduce CVE-2026-39987, un RCE sin autenticación en el WebSocket de terminal de marimo. Compara versiones vulnerables y parcheadas con scripts PoC de menor impacto para investigación educativa de seguridad.
Laboratorio Docker solo-local para reproducir y comprender CVE-2026-39987 en marimo. Este proyecto compara un servicio marimo vulnerable con un servicio marimo parcheado lado a lado, y luego demuestra la diferencia usando un script de prueba de mínimo daño.
CVE-2026-39987 es una vulnerabilidad crítica de ejecución remota de código pre-autenticación en marimo, un framework de notebooks reactivos de Python.
El comportamiento vulnerable existe en el endpoint WebSocket de terminal:
/terminal/ws
En versiones afectadas, este endpoint puede ser alcanzado sin autenticación válida y puede crear una sesión de terminal interactiva. Un atacante no autenticado que pueda alcanzar un servidor de edición marimo vulnerable puede ejecutar comandos con los privilegios del proceso marimo.
Este laboratorio reproduce la vulnerabilidad en un entorno Docker local controlado:
| Servicio | Versión | URL | Comportamiento esperado |
|---|---|---|---|
vuln | marimo 0.20.4 | http://127.0.0.1:8081 | /terminal/ws acepta conexión WebSocket no autenticada |
patched | marimo 0.23.0 | http://127.0.0.1:8082 | /terminal/ws rechaza conexión WebSocket no autenticada con 403 Forbidden |
El objetivo no es proporcionar un exploit armado. El objetivo es mostrar la diferencia de seguridad observable entre versiones vulnerables y parcheadas usando evidencia reproducible y solo-local.
El componente afectado es el endpoint WebSocket de terminal de marimo:
/terminal/ws
Este endpoint es utilizado por el entorno de edición de marimo para proporcionar funcionalidad de terminal a través de una sesión WebSocket conectada al navegador.
El endpoint vulnerable aceptaba conexiones WebSocket sin aplicar las mismas comprobaciones de autenticación esperadas para la funcionalidad de edición protegida de marimo.
La diferencia importante es:
comportamiento vulnerable:
un cliente no autenticado puede conectarse a /terminal/ws
se crea una sesión de terminal
se pueden enviar comandos a través del WebSocket
comportamiento parcheado:
un cliente no autenticado es rechazado
el handshake WebSocket falla con 403 Forbidden
no se crea una sesión de terminal
El parche añade validación de autenticación al flujo WebSocket de terminal antes de permitir que se establezca una sesión de terminal.
Si un servidor de edición marimo vulnerable está expuesto a una red alcanzable, un atacante no autenticado puede ejecutar comandos como el usuario que ejecuta el proceso marimo.
En este laboratorio, el proceso marimo se ejecuta intencionalmente como un usuario no root de bajos privilegios:
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
Esto mantiene la demostración más segura mientras sigue probando la vulnerabilidad.
Este laboratorio está diseñado para probar tres cosas:
/terminal/ws.403 Forbidden.El laboratorio evita intencionalmente comandos destructivos, persistencia, reverse shells, robo de credenciales o cualquier objetivo expuesto a internet.
.
├── docker-compose.yml
├── vuln/
│ ├── Dockerfile
│ └── notebook.py
├── patched/
│ ├── Dockerfile
│ └── notebook.py
├── poc/
│ ├── poc.py
│ ├── rce_poc.py
│ └── requirements.txt
├── SAFETY.md
├── README.md
└── .gitignore
| Archivo | Propósito |
|---|---|
docker-compose.yml | Define los servicios marimo vulnerable y parcheado |
vuln/Dockerfile | Construye el servicio marimo vulnerable |
patched/Dockerfile | Construye el servicio marimo parcheado |
vuln/notebook.py | Archivo de notebook marimo mínimo utilizado por el servicio vulnerable |
patched/notebook.py | Archivo de notebook marimo mínimo utilizado por el servicio parcheado |
poc/poc.py | Script de prueba de mínimo daño que ejecuta solo comandos benignos |
poc/rce_poc.py | Cliente de aprendizaje en tiempo real para observar el comportamiento de la terminal en el laboratorio local |
poc/requirements.txt | Dependencias de Python para los scripts PoC |
SAFETY.md | Reglas de seguridad y límites de alcance |
Máquina anfitriona
127.0.0.1:8081 ─────► contenedor vuln
marimo 0.20.4
/terminal/ws acepta WebSocket no autenticado
127.0.0.1:8082 ─────► contenedor patched
marimo 0.23.0
/terminal/ws rechaza WebSocket no autenticado
Ambos servicios exponen el puerto interno 2718 de marimo, pero los puertos del anfitrión son diferentes:
vuln -> 127.0.0.1:8081
patched -> 127.0.0.1:8082
Los servicios están vinculados solo a 127.0.0.1. No están destinados a ser expuestos a una LAN o a internet.
Los contenedores están configurados con varias salvaguardas donde es posible:
- vincular puertos solo a 127.0.0.1
- ejecutar como usuario no root
- eliminar capacidades de Linux
- habilitar no-new-privileges
- usar un sistema de archivos raíz de solo lectura
- proporcionar solo ubicaciones tmpfs de escritura limitadas
- aislar servicios dentro de una red puente Docker dedicada
Estos controles no eliminan la vulnerabilidad del servicio vulnerable. Reducen el radio de explosión de la demostración local.
Suposiciones probadas:
- Linux x86_64 o macOS con Docker Desktop
- Docker Compose v2
- Python 3.9+
- Pruebas solo en localhost
Herramientas requeridas:
docker --version
docker compose version
python3 --version
Clona o entra en el directorio del proyecto:
cd cve-2026-39987
Construye e inicia ambos servicios:
docker compose up -d --build
Comprueba que ambos contenedores están en ejecución:
docker compose ps
Servicios esperados:
cve-2026-39987-vuln
cve-2026-39987-patched
Comprueba la versión del servicio vulnerable:
docker compose exec vuln marimo --version
Esperado:
0.20.4
Comprueba la versión del servicio parcheado:
docker compose exec patched marimo --version
Esperado:
0.23.0
Crea un entorno virtual de Python:
python3 -m venv .venv
source .venv/bin/activate
Instala las dependencias:
python -m pip install -r poc/requirements.txt
El script de prueba principal es:
poc/poc.py
Intenta conectarse a:
/terminal/ws
Luego envía solo comandos de prueba benignos:
id
whoami
hostname
No se utiliza reverse shell, escritura de archivos, persistencia, acceso a credenciales ni comandos destructivos.
Ejecuta: