
Lab Docker local reproduzindo CVE-2026-39987, um RCE pré-autenticação no WebSocket do terminal do marimo. Compara versões vulneráveis e corrigidas com scripts PoC de menor dano para pesquisa educacional em segurança.
Laboratório Docker somente local para reproduzir e entender a CVE-2026-39987 no marimo. Este projeto compara um serviço marimo vulnerável com um serviço marimo corrigido lado a lado e, em seguida, demonstra a diferença usando um script de prova de menor dano.
A CVE-2026-39987 é uma vulnerabilidade crítica de execução remota de código pré-autenticação no marimo, um framework reativo de notebooks Python.
O comportamento vulnerável existe no endpoint WebSocket de terminal:
/terminal/ws
Em versões afetadas, este endpoint pode ser acessado sem autenticação válida e pode criar uma sessão de terminal interativa. Um atacante não autenticado que consiga alcançar um servidor de edição marimo vulnerável pode executar comandos com os privilégios do processo marimo.
Este laboratório reproduz a vulnerabilidade em um ambiente Docker local controlado:
| Serviço | Versão | URL | Comportamento esperado |
|---|---|---|---|
vuln | marimo 0.20.4 | http://127.0.0.1:8081 | /terminal/ws aceita conexão WebSocket não autenticada |
patched | marimo 0.23.0 | http://127.0.0.1:8082 | /terminal/ws rejeita conexão WebSocket não autenticada com 403 Forbidden |
O objetivo não é fornecer um exploit armado. O objetivo é mostrar a diferença de segurança observável entre versões vulneráveis e corrigidas usando evidências reproduzíveis e somente locais.
O componente afetado é o endpoint WebSocket de terminal do marimo:
/terminal/ws
Este endpoint é usado pelo ambiente de edição do marimo para fornecer funcionalidade de terminal através de uma sessão WebSocket conectada ao navegador.
O endpoint vulnerável aceitava conexões WebSocket sem aplicar as mesmas verificações de autenticação esperadas para a funcionalidade de edição protegida do marimo.
A diferença importante é:
comportamento vulnerável:
cliente não autenticado pode conectar-se a /terminal/ws
a sessão de terminal é criada
comandos podem ser enviados através do WebSocket
comportamento corrigido:
cliente não autenticado é rejeitado
o handshake WebSocket falha com 403 Forbidden
a sessão de terminal não é criada
O patch adiciona validação de autenticação ao fluxo do WebSocket de terminal antes de permitir que uma sessão de terminal seja estabelecida.
Se um servidor de edição marimo vulnerável for exposto a uma rede alcançável, um atacante não autenticado pode executar comandos como o usuário que executa o processo marimo.
Neste laboratório, o processo marimo é executado intencionalmente como um usuário não-root de baixo privilégio:
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
Isso mantém a demonstração mais segura enquanto ainda prova a vulnerabilidade.
Este laboratório foi projetado para provar três coisas:
/terminal/ws.403 Forbidden.O laboratório evita intencionalmente comandos destrutivos, persistência, reverse shells, extração de credenciais ou qualquer alvo exposto à 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
| Arquivo | Finalidade |
|---|---|
docker-compose.yml | Define os serviços marimo vulnerável e corrigido |
vuln/Dockerfile | Constrói o serviço marimo vulnerável |
patched/Dockerfile | Constrói o serviço marimo corrigido |
vuln/notebook.py | Arquivo mínimo de notebook marimo usado pelo serviço vulnerável |
patched/notebook.py | Arquivo mínimo de notebook marimo usado pelo serviço corrigido |
poc/poc.py | Script de prova de menor dano que executa apenas comandos benignos |
poc/rce_poc.py | Cliente de aprendizado em tempo real para observar o comportamento do terminal no laboratório local |
poc/requirements.txt | Dependências Python para os scripts de PoC |
SAFETY.md | Regras de segurança e limites de escopo |
Máquina host
127.0.0.1:8081 ─────► contêiner vuln
marimo 0.20.4
/terminal/ws aceita WebSocket não autenticado
127.0.0.1:8082 ─────► contêiner patched
marimo 0.23.0
/terminal/ws rejeita WebSocket não autenticado
Ambos os serviços expõem a porta interna 2718 do marimo, mas as portas do host são diferentes:
vuln -> 127.0.0.1:8081
patched -> 127.0.0.1:8082
Os serviços estão vinculados apenas a 127.0.0.1. Eles não se destinam a ser expostos a uma LAN ou à internet.
Os contêineres são configurados com várias salvaguardas onde possível:
- vincular portas apenas a 127.0.0.1
- executar como usuário não-root
- remover capacidades Linux
- habilitar no-new-privileges
- usar sistema de arquivos raiz somente leitura
- fornecer apenas locais de escrita tmpfs limitados
- isolar serviços dentro de uma rede bridge Docker dedicada
Esses controles não removem a vulnerabilidade do serviço vulnerável. Eles reduzem o raio de explosão da demonstração local.
Premissas testadas:
- Linux x86_64 ou macOS com Docker Desktop
- Docker Compose v2
- Python 3.9+
- Testes somente em localhost
Ferramentas necessárias:
docker --version
docker compose version
python3 --version
Clone ou entre no diretório do projeto:
cd cve-2026-39987
Construa e inicie ambos os serviços:
docker compose up -d --build
Verifique se ambos os contêineres estão em execução:
docker compose ps
Serviços esperados:
cve-2026-39987-vuln
cve-2026-39987-patched
Verifique a versão do serviço vulnerável:
docker compose exec vuln marimo --version
Esperado:
0.20.4
Verifique a versão do serviço corrigido:
docker compose exec patched marimo --version
Esperado:
0.23.0
Crie um ambiente virtual Python:
python3 -m venv .venv
source .venv/bin/activate
Instale as dependências:
python -m pip install -r poc/requirements.txt
O script de prova principal é:
poc/poc.py
Ele tenta conectar-se a:
/terminal/ws
Em seguida, envia apenas comandos de prova benignos:
id
whoami
hostname
Nenhum reverse shell, escrita de arquivo, persistência, acesso a credenciais ou comando destrutivo é usado.
Execute:
python poc/poc.py --base-url http://127.0.0.1:8081
Resultado esperado: