
# Laboratoire Docker local reproduisant CVE-2026-39987, une RCE sans authentification dans le WebSocket du terminal de marimo. Compare les versions vulnérable et corrigée avec des scripts PoC à moindre impact pour la recherche en sécurité à des fins éducatives.
Laboratoire Docker local uniquement pour reproduire et comprendre CVE-2026-39987 dans marimo. Ce projet compare un service marimo vulnérable avec un service marimo corrigé côte à côte, puis démontre la différence à l'aide d'un script de preuve à moindre impact.
CVE-2026-39987 est une vulnérabilité critique d'exécution de code à distance avant authentification dans marimo, un framework de notebooks Python réactif.
Le comportement vulnérable existe dans le point de terminaison WebSocket du terminal :
/terminal/ws
Dans les versions affectées, ce point de terminaison peut être atteint sans authentification valide et peut créer une session de terminal interactive. Un attaquant non authentifié qui peut atteindre un serveur d'édition marimo vulnérable peut exécuter des commandes avec les privilèges du processus marimo.
Ce laboratoire reproduit la vulnérabilité dans un environnement Docker local contrôlé :
| Service | Version | URL | Comportement attendu |
|---|---|---|---|
vuln | marimo 0.20.4 | http://127.0.0.1:8081 | /terminal/ws accepte la connexion WebSocket non authentifiée |
patched | marimo 0.23.0 | http://127.0.0.1:8082 | /terminal/ws rejette la connexion WebSocket non authentifiée avec 403 Forbidden |
L'objectif n'est pas de fournir un exploit armé. L'objectif est de montrer la différence de sécurité observable entre les versions vulnérable et corrigée en utilisant des preuves reproductibles et locales uniquement.
Le composant affecté est le point de terminaison WebSocket du terminal de marimo :
/terminal/ws
Ce point de terminaison est utilisé par l'environnement d'édition de marimo pour fournir des fonctionnalités de terminal via une session WebSocket connectée au navigateur.
Le point de terminaison vulnérable acceptait les connexions WebSocket sans appliquer les mêmes contrôles d'authentification attendus pour les fonctionnalités d'édition marimo protégées.
La différence importante est :
comportement vulnérable :
un client non authentifié peut se connecter à /terminal/ws
une session de terminal est créée
des commandes peuvent être envoyées via le WebSocket
comportement corrigé :
le client non authentifié est rejeté
la poignée de main WebSocket échoue avec 403 Forbidden
aucune session de terminal n'est créée
Le correctif ajoute une validation d'authentification au flux WebSocket du terminal avant de permettre l'établissement d'une session de terminal.
Si un serveur d'édition marimo vulnérable est exposé à un réseau accessible, un attaquant non authentifié peut exécuter des commandes en tant qu'utilisateur exécutant le processus marimo.
Dans ce laboratoire, le processus marimo est intentionnellement exécuté en tant qu'utilisateur non-root à privilèges réduits :
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
Cela rend la démonstration plus sûre tout en prouvant la vulnérabilité.
Ce laboratoire est conçu pour prouver trois choses :
/terminal/ws.403 Forbidden.Le laboratoire évite intentionnellement les commandes destructrices, la persistance, les shells inversés, l'exfiltration d'identifiants ou toute cible exposée à 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
| Fichier | Objectif |
|---|---|
docker-compose.yml | Définit les services marimo vulnérable et corrigé |
vuln/Dockerfile | Construit le service marimo vulnérable |
patched/Dockerfile | Construit le service marimo corrigé |
vuln/notebook.py | Fichier notebook marimo minimal utilisé par le service vulnérable |
patched/notebook.py | Fichier notebook marimo minimal utilisé par le service corrigé |
poc/poc.py | Script de preuve à moindre impact qui exécute uniquement des commandes bénignes |
poc/rce_poc.py | Client d'apprentissage en temps réel pour observer le comportement du terminal dans le laboratoire local |
poc/requirements.txt | Dépendances Python pour les scripts PoC |
SAFETY.md | Règles de sécurité et limites de portée |
Machine hôte
127.0.0.1:8081 ─────► conteneur vuln
marimo 0.20.4
/terminal/ws accepte le WebSocket non authentifié
127.0.0.1:8082 ─────► conteneur patched
marimo 0.23.0
/terminal/ws rejette le WebSocket non authentifié
Les deux services exposent le port interne 2718 de marimo, mais les ports hôte sont différents :
vuln -> 127.0.0.1:8081
patched -> 127.0.0.1:8082
Les services sont liés uniquement à 127.0.0.1. Ils ne sont pas destinés à être exposés à un LAN ou à Internet.
Les conteneurs sont configurés avec plusieurs garde-fous lorsque cela est possible :
- lier les ports uniquement à 127.0.0.1
- exécuter en tant qu'utilisateur non-root
- supprimer les capacités Linux
- activer no-new-privileges
- utiliser un système de fichiers racine en lecture seule
- fournir uniquement des emplacements d'écriture tmpfs limités
- isoler les services dans un réseau pont Docker dédié
Ces contrôles ne suppriment pas la vulnérabilité du service vulnérable. Ils réduisent le rayon d'explosion de la démonstration locale.
Hypothèses testées :
- Linux x86_64 ou macOS avec Docker Desktop
- Docker Compose v2
- Python 3.9+
- Tests en localhost uniquement
Outils requis :
docker --version
docker compose version
python3 --version
Clonez ou entrez dans le répertoire du projet :
cd cve-2026-39987
Construisez et démarrez les deux services :
docker compose up -d --build
Vérifiez que les deux conteneurs sont en cours d'exécution :
docker compose ps
Services attendus :
cve-2026-39987-vuln
cve-2026-39987-patched
Vérifiez la version du service vulnérable :
docker compose exec vuln marimo --version
Attendu :
0.20.4
Vérifiez la version du service corrigé :
docker compose exec patched marimo --version
Attendu :
0.23.0
Créez un environnement virtuel Python :
python3 -m venv .venv
source .venv/bin/activate
Installez les dépendances :
python -m pip install -r poc/requirements.txt
Le script de preuve principal est :
poc/poc.py
Il tente de se connecter à :
/terminal/ws