
# 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
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:
python poc/poc.py --base-url http://127.0.0.1:8081
Resultado esperado:
[*] Target WebSocket: ws://127.0.0.1:8081/terminal/ws
[*] Sending benign proof command only: id; whoami; hostname
[+] websocket connected without credentials
[result] VULNERABLE: unauthenticated command execution observed
[proof]
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<container-hostname>
Si el script imprime la salida de terminal sin procesar pero incluye este bloque, la vulnerabilidad sigue confirmada:
CVE39987_PROOF_START
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<container-hostname>
CVE39987_PROOF_END
Eso significa que la conexión WebSocket tuvo éxito y la salida del comando volvió de la sesión de terminal.
Ejecuta:
python poc/poc.py --base-url http://127.0.0.1:8082
Resultado esperado:
[*] Target WebSocket: ws://127.0.0.1:8082/terminal/ws
[*] Sending benign proof command only: id; whoami; hostname
[-] websocket connection rejected/failed: Handshake status 403 Forbidden
[result] not exploitable by this unauthenticated check
Esto muestra que el servicio parcheado rechaza el acceso no autenticado antes de crear una sesión de terminal.
El archivo siguiente está destinado solo al aprendizaje local:
poc/rce_poc.py
Demuestra el mismo problema de una manera más en tiempo real conectándose al WebSocket de terminal vulnerable y permitiendo la interacción con la terminal.
Úsalo solo contra el laboratorio Docker local.
python poc/rce_poc.py --base-url http://127.0.0.1:8081
Comportamiento esperado:
[+] Connected successfully (Pre-Auth RCE)
✓ Interactive shell is ready to use!
Comandos seguros para probar:
id
whoami
hostname
pwd
python -c 'import marimo; print(marimo.__version__)'
exit
python poc/rce_poc.py --base-url http://127.0.0.1:8082
Comportamiento esperado:
[-] Connection Failed: 403 Forbidden
[!] This is likely the PATCHED version.
The WebSocket endpoint is now protected.
El cliente en tiempo real existe para ayudar a comprender cómo se comporta el WebSocket vulnerable. No debe usarse contra ningún sistema fuera de este laboratorio local.
Para la publicación en portafolio, la evidencia principal recomendada sigue siendo poc/poc.py, porque es acotada, repetible y de mínimo daño.
El resultado principal del laboratorio debe resumirse como:
vuln / marimo 0.20.4:
el handshake WebSocket no autenticado a /terminal/ws tiene éxito
la salida de comandos benignos es observable
patched / marimo 0.23.0:
el handshake WebSocket no autenticado a /terminal/ws falla
el servidor devuelve 403 Forbidden
no se crea ninguna sesión de terminal
Esta es la prueba central para la reproducción del CVE.
Si quieres inspeccionar manualmente el comportamiento, usa un cliente compatible con WebSocket y conéctate a:
ws://127.0.0.1:8081/terminal/ws
ws://127.0.0.1:8082/terminal/ws
Esperado:
8081 -> conexión aceptada
8082 -> 403 Forbidden
Los scripts PoC son preferibles porque producen evidencia más clara.
En un entorno real, los indicadores útiles incluyen:
- solicitudes WebSocket a /terminal/ws
- intentos de acceso no autenticado a servidores de edición marimo
- sesiones de terminal inesperadas generadas por marimo
- comandos lanzados por el proceso marimo
- acceso a .env, claves SSH, credenciales en la nube o secretos de notebooks
- tráfico de red saliente poco después de conexiones a /terminal/ws
Ejemplos de artefactos locales para inspeccionar:
docker compose logs vuln
docker compose logs patched
docker compose ps
docker compose exec vuln ps aux
Detén y elimina los contenedores:
docker compose down
Elimina también los volúmenes:
docker compose down -v
Elimina el entorno virtual de Python local si lo deseas:
rm -rf .venv
Este repositorio es solo para investigación de seguridad local y demostración de portafolio.
Permitido:
- pruebas en localhost
- reproducción solo con Docker
- comandos de prueba benignos como id, whoami, hostname
- comparar el comportamiento vulnerable y parcheado
- documentar la causa raíz e ideas de detección
No permitido:
- probar contra servidores marimo públicos
- probar sistemas que no posees o administras
- reverse shells
- persistencia
- robo de credenciales
- comandos destructivos
- movimiento lateral
- comportamiento de botnet o malware
Este proyecto está destinado únicamente a pruebas locales autorizadas y educación defensiva en seguridad. No uses estos scripts contra sistemas que no poseas o para los que no tengas permiso explícito de prueba.
Aviso de Seguridad de GitHub — GHSA-2679-6mx9-h9xc:
https://github.com/advisories/GHSA-2679-6mx9-h9xc
NVD — CVE-2026-39987:
https://nvd.nist.gov/vuln/detail/CVE-2026-39987
Repositorio upstream de marimo:
https://github.com/marimo-team/marimo
Commit del parche — añade validación de autenticación al WebSocket de terminal:
https://github.com/marimo-team/marimo/commit/c24d4806398f30be6b12acd6c60d1d7c68cfd12a
PR del parche — marimo PR #9098:
https://github.com/marimo-team/marimo/pull/9098
| 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 |