
# Локальная 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
Хост-машина
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, записи файлов, персистентности, доступа к учётным данным или разрушительных команд не используется.
Запустите:
python poc/poc.py --base-url http://127.0.0.1:8081
Ожидаемый результат:
[*] 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>
Если скрипт выводит необработанный терминальный вывод, но включает этот блок, уязвимость всё равно подтверждена:
CVE39987_PROOF_START
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<container-hostname>
CVE39987_PROOF_END
Это означает, что WebSocket-соединение прошло успешно, и вывод команд вернулся из терминальной сессии.
Запустите:
python poc/poc.py --base-url http://127.0.0.1:8082
Ожидаемый результат:
[*] 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
Это показывает, что пропатченный сервис отклоняет неаутентифицированный доступ до создания терминальной сессии.
Файл ниже предназначен только для локального обучения:
poc/rce_poc.py
Он демонстрирует ту же проблему в более наглядном режиме реального времени, подключаясь к уязвимому терминальному WebSocket и позволяя взаимодействовать с терминалом.
Используйте его только против локальной Docker-лаборатории.
python poc/rce_poc.py --base-url http://127.0.0.1:8081
Ожидаемое поведение:
[+] Connected successfully (Pre-Auth RCE)
✓ Interactive shell is ready to use!
Безопасные команды для проверки:
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
Ожидаемое поведение:
[-] Connection Failed: 403 Forbidden
[!] This is likely the PATCHED version.
The WebSocket endpoint is now protected.
Клиент реального времени существует, чтобы помочь понять, как ведёт себя уязвимый WebSocket. Его не следует использовать против любых систем за пределами этой локальной лаборатории.
Для публикации в портфолио рекомендуемым основным доказательством остаётся poc/poc.py, поскольку он ограничен, воспроизводим и наносит минимальный вред.
Основной результат лаборатории можно резюмировать так:
vuln / marimo 0.20.4:
неаутентифицированное WebSocket-рукопожатие к /terminal/ws успешно
вывод безвредных команд наблюдаем
patched / marimo 0.23.0:
неаутентифицированное WebSocket-рукопожатие к /terminal/ws завершается ошибкой
сервер возвращает 403 Forbidden
терминальная сессия не создаётся
Это основное доказательство воспроизведения CVE.
Если вы хотите вручную проверить поведение, используйте WebSocket-совместимый клиент и подключитесь к:
ws://127.0.0.1:8081/terminal/ws
ws://127.0.0.1:8082/terminal/ws
Ожидаемый результат:
8081 -> соединение принято
8082 -> 403 Forbidden
PoC-скрипты предпочтительнее, поскольку они дают более чёткие доказательства.
В реальной среде полезными индикаторами являются:
- WebSocket-запросы к /terminal/ws
- попытки неаутентифицированного доступа к серверам редактирования marimo
- неожиданные терминальные сессии, порождённые marimo
- команды, запускаемые процессом marimo
- доступ к .env, SSH-ключам, облачным учётным данным или секретам ноутбуков
- исходящий сетевой трафик вскоре после подключений к /terminal/ws
Примеры локальных артефактов для проверки:
docker compose logs vuln
docker compose logs patched
docker compose ps
docker compose exec vuln ps aux
Остановите и удалите контейнеры:
docker compose down
Также удалите тома:
docker compose down -v
При желании удалите локальное виртуальное окружение Python:
rm -rf .venv
Этот репозиторий предназначен только для локальных исследований в области безопасности и демонстрации в портфолио.
Разрешено:
- тестирование на localhost
- воспроизведение только в Docker
- безвредные команды доказательства, такие как id, whoami, hostname
- сравнение уязвимого и пропатченного поведения
- документирование корневой причины и идей обнаружения
Запрещено:
- тестирование публичных серверов marimo
- тестирование систем, которыми вы не владеете или не администрируете
- reverse shell
- персистентность
- кража учётных данных
- разрушительные команды
- боковое перемещение
- поведение ботнета или вредоносного ПО
Этот проект предназначен только для авторизованного локального тестирования и обучения защитным практикам в области безопасности. Не используйте эти скрипты против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения.
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
Вышестоящий репозиторий marimo:
https://github.com/marimo-team/marimo
Коммит патча — добавление проверки аутентификации в терминальный WebSocket:
https://github.com/marimo-team/marimo/commit/c24d4806398f30be6b12acd6c60d1d7c68cfd12a
PR с патчем — marimo PR #9098:
https://github.com/marimo-team/marimo/pull/9098
| Файл | Назначение |
|---|
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 | Правила безопасности и границы области применения |