
# Локальная Docker-лаборатория, воспроизводящая CVE-2026-39987 — RCE без аутентификации в терминальном WebSocket-соединении marimo. Сравнивает уязвимую и исправленную версии с PoC-скриптами минимального воздействия для образовательных исследований в области безопасности.
Локальная Docker-лаборатория для воспроизведения и понимания CVE-2026-39987 в marimo. Этот проект сравнивает уязвимый сервис marimo с пропатченным сервисом marimo бок о бок, а затем демонстрирует разницу с помощью скрипта доказательства с минимальным вредом.
CVE-2026-39987 — это критическая уязвимость удалённого выполнения кода до аутентификации в marimo, реактивном Python-фреймворке для ноутбуков.
Уязвимое поведение существует в конечной точке терминального WebSocket:
/terminal/ws
В затронутых версиях эта конечная точка доступна без действительной аутентификации и может создать интерактивную терминальную сессию. Неаутентифицированный злоумышленник, имеющий доступ к уязвимому серверу редактирования marimo, может выполнять команды с привилегиями процесса marimo.
Эта лаборатория воспроизводит уязвимость в контролируемой локальной Docker-среде:
| Сервис | Версия | URL | Ожидаемое поведение |
|---|---|---|---|
vuln | marimo 0.20.4 | http://127.0.0.1:8081 | /terminal/ws принимает неаутентифицированное WebSocket-соединение |
patched | marimo 0.23.0 | http://127.0.0.1:8082 | /terminal/ws отклоняет неаутентифицированное WebSocket-соединение с 403 Forbidden |
Цель — не предоставить боевой эксплойт. Цель — показать наблюдаемую разницу в безопасности между уязвимой и пропатченной версиями, используя только локальные, воспроизводимые доказательства.
Затронутый компонент — это конечная точка терминального WebSocket в marimo:
/terminal/ws
Эта конечная точка используется средой редактирования marimo для предоставления терминальной функциональности через WebSocket-сессию, подключённую через браузер.
Уязвимая конечная точка принимала WebSocket-соединения без применения тех же проверок аутентификации, которые ожидаются для защищённой функциональности редактирования marimo.
Ключевое различие:
уязвимое поведение:
неаутентифицированный клиент может подключиться к /terminal/ws
создаётся терминальная сессия
команды могут отправляться через WebSocket
пропатченное поведение:
неаутентифицированный клиент отклоняется
WebSocket-рукопожатие завершается ошибкой 403 Forbidden
терминальная сессия не создаётся
Патч добавляет проверку аутентификации в поток терминального WebSocket перед тем, как разрешить установление терминальной сессии.
Если уязвимый сервер редактирования marimo доступен из достижимой сети, неаутентифицированный злоумышленник может выполнять команды от имени пользователя, запускающего процесс marimo.
В этой лаборатории процесс marimo намеренно запускается от непривилегированного пользователя, не являющегося root:
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
Это делает демонстрацию более безопасной, при этом доказывая уязвимость.
Эта лаборатория предназначена для доказательства трёх вещей:
/terminal/ws.403 Forbidden.Лаборатория намеренно избегает разрушительных команд, персистентности, reverse shell, кражи учётных данных и любых целей, доступных из интернета.
.
├── docker-compose.yml
├── vuln/
│ ├── Dockerfile
│ └── notebook.py
├── patched/
│ ├── Dockerfile
│ └── notebook.py
├── poc/
│ ├── poc.py
│ ├── rce_poc.py
│ └── requirements.txt
├── SAFETY.md
├── README.md
└── .gitignore
| Файл | Назначение |
|---|---|
docker-compose.yml | Определяет уязвимый и пропатченный сервисы marimo |
vuln/Dockerfile | Собирает уязвимый сервис marimo |
patched/Dockerfile | Собирает пропатченный сервис marimo |
vuln/notebook.py | Минимальный файл ноутбука marimo, используемый уязвимым сервисом |
patched/notebook.py | Минимальный файл ноутбука marimo, используемый пропатченным сервисом |
poc/poc.py | Скрипт доказательства с минимальным вредом, выполняющий только безвредные команды |
poc/rce_poc.py | Клиент для обучения в реальном времени для наблюдения за поведением терминала в локальной лаборатории |
poc/requirements.txt | Python-зависимости для PoC-скриптов |
SAFETY.md | Правила безопасности и границы области применения |
Хост-машина
127.0.0.1:8081 ─────► контейнер vuln
marimo 0.20.4
/terminal/ws принимает неаутентифицированный WebSocket
127.0.0.1:8082 ─────► контейнер patched
marimo 0.23.0
/terminal/ws отклоняет неаутентифицированный WebSocket
Оба сервиса открывают внутренний порт marimo 2718, но порты хоста разные:
vuln -> 127.0.0.1:8081
patched -> 127.0.0.1:8082
Сервисы привязаны только к 127.0.0.1. Они не предназначены для доступа из локальной сети или интернета.
Контейнеры настроены с несколькими защитными ограничениями, где это возможно:
- привязка портов только к 127.0.0.1
- запуск от непривилегированного пользователя
- удаление Linux-возможностей (capabilities)
- включение no-new-privileges
- использование read-only корневой файловой системы
- предоставление только ограниченных tmpfs-областей для записи
- изоляция сервисов внутри выделенной Docker bridge-сети
Эти меры не устраняют уязвимость в уязвимом сервисе. Они уменьшают радиус поражения локальной демонстрации.
Проверенные предположения:
- Linux x86_64 или macOS с Docker Desktop
- Docker Compose v2
- Python 3.9+
- Тестирование только на localhost
Необходимые инструменты:
docker --version
docker compose version
python3 --version
Клонируйте или войдите в каталог проекта:
cd cve-2026-39987
Соберите и запустите оба сервиса:
docker compose up -d --build
Проверьте, что оба контейнера запущены:
docker compose ps
Ожидаемые сервисы:
cve-2026-39987-vuln
cve-2026-39987-patched
Проверьте версию уязвимого сервиса:
docker compose exec vuln marimo --version
Ожидаемый результат:
0.20.4
Проверьте версию пропатченного сервиса:
docker compose exec patched marimo --version
Ожидаемый результат:
0.23.0
Создайте виртуальное окружение Python:
python3 -m venv .venv
source .venv/bin/activate
Установите зависимости:
python -m pip install -r poc/requirements.txt
Основной скрипт доказательства:
poc/poc.py
Он пытается подключиться к:
/terminal/ws
Затем отправляет только безвредные команды доказательства:
id
whoami
hostname
Никакого reverse shell, записи файлов, персистентности, доступа к учётным данным или разрушительных команд не используется.
Запустите: