仅限本地的 Docker 实验环境,用于复现和理解 marimo 中的 CVE-2026-39987。 本项目将存在漏洞的 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 以 403 Forbidden 拒绝未认证的 WebSocket 连接 |
目标不是提供武器化的漏洞利用代码。目标是使用仅限本地的、可复现的证据,展示存在漏洞版本与已修复版本之间可观察到的安全差异。
受影响的组件是 marimo 的终端 WebSocket 端点:
/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 的未认证 WebSocket 连接。403 Forbidden 拒绝相同的未认证 WebSocket 尝试。本实验环境刻意避免破坏性命令、持久化、反向 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
- 以非 root 用户身份运行
- 丢弃 Linux 能力
- 启用 no-new-privileges
- 使用只读根文件系统
- 仅提供有限的 tmpfs 写入位置
- 将服务隔离在专用的 Docker 桥接网络中
这些控制措施不会消除存在漏洞服务中的漏洞。它们减少了本地演示的爆炸半径。
已测试的假设:
- Linux x86_64 或带 Docker Desktop 的 macOS
- 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
不使用反向 Shell、文件写入、持久化、凭据访问或破坏性命令。
运行:
python poc/poc.py --base-url http://127.0.0.1:8081
预期结果:
[*] 目标 WebSocket: ws://127.0.0.1:8081/terminal/ws
[*] 仅发送良性证明命令: id; whoami; hostname
[+] websocket 无需凭据即可连接
[result] 存在漏洞: 观察到未认证的命令执行
[proof]
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<容器主机名>
如果脚本打印原始终端输出但包含以下块,则漏洞仍然得到确认:
CVE39987_PROOF_START
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<容器主机名>
CVE39987_PROOF_END
这意味着 WebSocket 连接成功,并且命令输出从终端会话返回。
运行:
python poc/poc.py --base-url http://127.0.0.1:8082
预期结果:
[*] 目标 WebSocket: ws://127.0.0.1:8082/terminal/ws
[*] 仅发送良性证明命令: id; whoami; hostname
[-] websocket 连接被拒绝/失败: 握手状态 403 Forbidden
[result] 此未认证检查无法利用
这表明已修复的服务在创建终端会话之前拒绝未认证访问。
以下文件仅用于本地学习:
poc/rce_poc.py
它通过连接到存在漏洞的终端 WebSocket 并允许终端交互,以更实时的方式演示相同的问题。
仅可针对本地 Docker 实验环境使用。
python poc/rce_poc.py --base-url http://127.0.0.1:8081
预期行为:
[+] 连接成功(预认证 RCE)
✓ 交互式 Shell 已准备好使用!
可尝试的安全命令:
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
预期行为:
[-] 连接失败: 403 Forbidden
[!] 这很可能是已修复的版本。
WebSocket 端点现已受到保护。
实时客户端的存在是为了帮助理解存在漏洞的 WebSocket 的行为方式。它不应针对本本地实验环境之外的任何系统使用。
对于作品集发布,推荐的主要证据仍然是 poc/poc.py,因为它是有界的、可重复的且危害最小的。
主要实验环境结果应总结为:
vuln / marimo 0.20.4:
对 /terminal/ws 的未认证 WebSocket 握手成功
可以观察到良性命令输出
patched / marimo 0.23.0:
对 /terminal/ws 的未认证 WebSocket 握手失败
服务器返回 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 脚本是首选,因为它们产生更清晰的证据。
在真实环境中,有用的指标包括:
- 对 /terminal/ws 的 WebSocket 请求
- 对 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 服务器进行测试
- 测试您不拥有或管理的系统
- 反向 Shell
- 持久化
- 凭据窃取
- 破坏性命令
- 横向移动
- 僵尸网络或恶意软件行为
本项目仅用于授权的本地测试和防御性安全教育。请勿针对您不拥有或未经明确许可测试的系统使用这些脚本。
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
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 | PoC 脚本的 Python 依赖项 |
SAFETY.md | 安全规则和范围边界 |