
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
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:
[*] 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>
Se o script imprimir a saída bruta do terminal, mas incluir este bloco, a vulnerabilidade ainda está confirmada:
CVE39987_PROOF_START
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<container-hostname>
CVE39987_PROOF_END
Isso significa que a conexão WebSocket foi bem-sucedida e a saída do comando retornou da sessão de terminal.
Execute:
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
Isso mostra que o serviço corrigido rejeita o acesso não autenticado antes de criar uma sessão de terminal.
O arquivo abaixo destina-se apenas ao aprendizado local:
poc/rce_poc.py
Ele demonstra o mesmo problema de forma mais em tempo real, conectando-se ao WebSocket de terminal vulnerável e permitindo interação com o terminal.
Use-o apenas contra o laboratório Docker local.
python poc/rce_poc.py --base-url http://127.0.0.1:8081
Comportamento esperado:
[+] Connected successfully (Pre-Auth RCE)
✓ Interactive shell is ready to use!
Comandos seguros para experimentar:
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
Comportamento esperado:
[-] Connection Failed: 403 Forbidden
[!] This is likely the PATCHED version.
The WebSocket endpoint is now protected.
O cliente em tempo real existe para ajudar a entender como o WebSocket vulnerável se comporta. Ele não deve ser usado contra qualquer sistema fora deste laboratório local.
Para publicação em portfólio, a evidência primária recomendada ainda é poc/poc.py, porque é limitada, repetível e de menor dano.
O principal resultado do laboratório deve ser resumido como:
vuln / marimo 0.20.4:
handshake WebSocket não autenticado para /terminal/ws é bem-sucedido
a saída de comando benigno é observável
patched / marimo 0.23.0:
handshake WebSocket não autenticado para /terminal/ws falha
o servidor retorna 403 Forbidden
nenhuma sessão de terminal é criada
Esta é a prova central para a reprodução da CVE.
Se você quiser inspecionar manualmente o comportamento, use um cliente compatível com WebSocket e conecte-se a:
ws://127.0.0.1:8081/terminal/ws
ws://127.0.0.1:8082/terminal/ws
Esperado:
8081 -> conexão aceita
8082 -> 403 Forbidden
Os scripts de PoC são preferidos porque produzem evidências mais claras.
Em um ambiente real, indicadores úteis incluem:
- solicitações WebSocket para /terminal/ws
- tentativas de acesso não autenticado a servidores de edição marimo
- sessões de terminal inesperadas geradas pelo marimo
- comandos lançados pelo processo marimo
- acesso a .env, chaves SSH, credenciais de nuvem ou segredos de notebook
- tráfego de rede de saída logo após conexões /terminal/ws
Exemplos de artefatos locais para inspecionar:
docker compose logs vuln
docker compose logs patched
docker compose ps
docker compose exec vuln ps aux
Pare e remova os contêineres:
docker compose down
Remova também os volumes:
docker compose down -v
Remova o ambiente virtual Python local, se desejado:
rm -rf .venv
Este repositório é apenas para pesquisa de segurança local e demonstração de portfólio.
Permitido:
- testes em localhost
- reprodução somente com Docker
- comandos de prova benignos como id, whoami, hostname
- comparação de comportamento vulnerável e corrigido
- documentação da causa raiz e ideias de detecção
Não permitido:
- testar contra servidores marimo públicos
- testar sistemas que você não possui ou administra
- reverse shells
- persistência
- roubo de credenciais
- comandos destrutivos
- movimento lateral
- comportamento de botnet ou malware
Este projeto destina-se apenas a testes locais autorizados e educação defensiva em segurança. Não use estes scripts contra sistemas que você não possui ou para os quais não tem permissão explícita para testar.
GitHub Security Advisory — 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
Repositório upstream do marimo:
https://github.com/marimo-team/marimo
Commit do patch — adicionar validação de autenticação ao WebSocket de terminal:
https://github.com/marimo-team/marimo/commit/c24d4806398f30be6b12acd6c60d1d7c68cfd12a
PR do patch — marimo PR #9098:
https://github.com/marimo-team/marimo/pull/9098
| 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 |