
# 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
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
Ensuite, il envoie uniquement des commandes de preuve bénignes :
id
whoami
hostname
Aucun shell inversé, écriture de fichier, persistance, accès aux identifiants ou commande destructrice n'est utilisé.
Exécutez :
python poc/poc.py --base-url http://127.0.0.1:8081
Résultat attendu :
[*] 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 le script affiche la sortie brute du terminal mais inclut ce bloc, la vulnérabilité est toujours confirmée :
CVE39987_PROOF_START
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<container-hostname>
CVE39987_PROOF_END
Cela signifie que la connexion WebSocket a réussi et que la sortie de commande est revenue de la session de terminal.
Exécutez :
python poc/poc.py --base-url http://127.0.0.1:8082
Résultat attendu :
[*] 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
Cela montre que le service corrigé rejette l'accès non authentifié avant de créer une session de terminal.
Le fichier ci-dessous est destiné uniquement à l'apprentissage local :
poc/rce_poc.py
Il démontre le même problème de manière plus temps réel en se connectant au WebSocket de terminal vulnérable et en permettant l'interaction avec le terminal.
Utilisez-le uniquement contre le laboratoire Docker local.
python poc/rce_poc.py --base-url http://127.0.0.1:8081
Comportement attendu :
[+] Connected successfully (Pre-Auth RCE)
✓ Interactive shell is ready to use!
Commandes sûres à essayer :
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
Comportement attendu :
[-] Connection Failed: 403 Forbidden
[!] This is likely the PATCHED version.
The WebSocket endpoint is now protected.
Le client temps réel existe pour aider à comprendre comment le WebSocket vulnérable se comporte. Il ne doit pas être utilisé contre un système en dehors de ce laboratoire local.
Pour la publication de portfolio, la preuve principale recommandée reste poc/poc.py, car elle est bornée, reproductible et à moindre impact.
Le principal résultat du laboratoire doit être résumé comme suit :
vuln / marimo 0.20.4 :
la poignée de main WebSocket non authentifiée vers /terminal/ws réussit
la sortie de commande bénigne est observable
patched / marimo 0.23.0 :
la poignée de main WebSocket non authentifiée vers /terminal/ws échoue
le serveur renvoie 403 Forbidden
aucune session de terminal n'est créée
C'est la preuve centrale de la reproduction du CVE.
Si vous souhaitez inspecter manuellement le comportement, utilisez un client compatible WebSocket et connectez-vous à :
ws://127.0.0.1:8081/terminal/ws
ws://127.0.0.1:8082/terminal/ws
Attendu :
8081 -> connexion acceptée
8082 -> 403 Forbidden
Les scripts PoC sont préférés car ils produisent des preuves plus claires.
Dans un environnement réel, les indicateurs utiles incluent :
- requêtes WebSocket vers /terminal/ws
- tentatives d'accès non authentifiées vers les serveurs d'édition marimo
- sessions de terminal inattendues générées par marimo
- commandes lancées par le processus marimo
- accès à .env, clés SSH, identifiants cloud ou secrets de notebook
- trafic réseau sortant peu après les connexions /terminal/ws
Exemples d'artefacts locaux à inspecter :
docker compose logs vuln
docker compose logs patched
docker compose ps
docker compose exec vuln ps aux
Arrêtez et supprimez les conteneurs :
docker compose down
Supprimez également les volumes :
docker compose down -v
Supprimez l'environnement virtuel Python local si souhaité :
rm -rf .venv
Ce dépôt est destiné uniquement à la recherche de sécurité locale et à la démonstration de portfolio.
Autorisé :
- tests en localhost
- reproduction uniquement via Docker
- commandes de preuve bénignes telles que id, whoami, hostname
- comparaison des comportements vulnérable et corrigé
- documentation de la cause racine et des idées de détection
Interdit :
- tests contre des serveurs marimo publics
- tests de systèmes que vous ne possédez pas ou n'administrez pas
- shells inversés
- persistance
- vol d'identifiants
- commandes destructrices
- mouvement latéral
- comportement de botnet ou de malware
Ce projet est destiné uniquement aux tests locaux autorisés et à l'éducation en sécurité défensive. N'utilisez pas ces scripts contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas l'autorisation explicite de tester.
Avis de sécurité 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
Dépôt en amont de marimo :
https://github.com/marimo-team/marimo
Commit du correctif — ajout de la validation d'authentification au WebSocket du terminal :
https://github.com/marimo-team/marimo/commit/c24d4806398f30be6b12acd6c60d1d7c68cfd12a
PR du correctif — marimo PR #9098 :
https://github.com/marimo-team/marimo/pull/9098
| 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 |