
# CVE-2026-39987 로컬 Docker 랩 marimo의 터미널 WebSocket에서 발생하는 사전 인증 RCE(원격 코드 실행) 취약점인 CVE-2026-39987을 재현하는 로컬 Docker 랩입니다. 취약한 버전과 패치된 버전을 비교하며, 교육용 보안 연구를 위한 최소 피해 PoC 스크립트를 제공합니다.
marimo의 CVE-2026-39987을 재현하고 이해하기 위한 로컬 전용 Docker 실습 환경입니다. 이 프로젝트는 취약한 marimo 서비스와 패치된 marimo 서비스를 나란히 비교한 다음, 최소 피해 증명 스크립트를 사용하여 차이를 보여줍니다.
CVE-2026-39987은 반응형 Python 노트북 프레임워크인 marimo의 심각한 사전 인증 원격 코드 실행 취약점입니다.
취약한 동작은 터미널 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으로 거부함 |
목표는 무기화된 익스플로잇을 제공하는 것이 아닙니다. 목표는 로컬 전용의 재현 가능한 증거를 사용하여 취약한 버전과 패치된 버전 사이의 관찰 가능한 보안 차이를 보여주는 것입니다.
영향받는 구성 요소는 marimo의 터미널 WebSocket 엔드포인트입니다:
/terminal/ws
이 엔드포인트는 marimo 편집 환경에서 브라우저 연결 WebSocket 세션을 통해 터미널 기능을 제공하는 데 사용됩니다.
취약한 엔드포인트는 보호된 marimo 편집 기능에 기대되는 동일한 인증 검사를 적용하지 않고 WebSocket 연결을 수락했습니다.
중요한 차이점은 다음과 같습니다:
취약한 동작:
인증되지 않은 클라이언트가 /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으로 거부합니다.이 실습 환경은 의도적으로 파괴적인 명령, 지속성, 리버스 셸, 자격 증명 덤프 또는 인터넷 노출 대상을 피합니다.
.
├── 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에만 바인딩됩니다. LAN 또는 인터넷에 노출되도록 설계되지 않았습니다.
컨테이너는 가능한 경우 여러 안전장치로 구성됩니다:
- 포트를 127.0.0.1에만 바인딩
- 비-root 사용자로 실행
- Linux capabilities 제거
- no-new-privileges 활성화
- 읽기 전용 루트 파일시스템 사용
- 제한된 tmpfs 쓰기 위치만 제공
- 전용 Docker 브리지 네트워크 내에서 서비스 격리
이러한 통제는 취약한 서비스에서 취약점을 제거하지 않습니다. 로컬 데모의 폭발 반경을 줄일 뿐입니다.
테스트된 가정:
- Docker Desktop이 있는 Linux x86_64 또는 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
리버스 셸, 파일 쓰기, 지속성, 자격 증명 접근 또는 파괴적인 명령은 사용되지 않습니다.
실행:
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:
/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 서버에 대한 테스트
- 소유하거나 관리하지 않는 시스템 테스트
- 리버스 셸
- 지속성
- 자격 증명 탈취
- 파괴적인 명령
- 측면 이동
- 봇넷 또는 악성코드 동작
이 프로젝트는 승인된 로컬 테스트 및 방어적 보안 교육 전용입니다. 소유하지 않거나 테스트할 명시적 권한이 없는 시스템에 대해 이러한 스크립트를 사용하지 마십시오.
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 서비스와 패치된 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 | 안전 규칙 및 범위 경계 |