From 0ebbe2a76cba47f06cffc6ea1a26213f4341820d Mon Sep 17 00:00:00 2001 From: MANGESH NAGNATWALSANE Date: Thu, 15 Oct 2026 06:36:45 +0000 Subject: [PATCH 1/2] Added testbed for SGLang CVE-2026-3059 --- sglang/CVE-2026-3059/README.md | 112 ++++++++++++++++++++++ sglang/CVE-2026-3059/docker-compose.yml | 30 ++++++ sglang/CVE-2026-3059/sglang_lab_server.py | 77 +++++++++++++++ sglang/CVE-2026-3059/verify_zmtp.py | 25 +++++ 4 files changed, 244 insertions(+) create mode 100644 sglang/CVE-2026-3059/README.md create mode 100644 sglang/CVE-2026-3059/docker-compose.yml create mode 100644 sglang/CVE-2026-3059/sglang_lab_server.py create mode 100644 sglang/CVE-2026-3059/verify_zmtp.py diff --git a/sglang/CVE-2026-3059/README.md b/sglang/CVE-2026-3059/README.md new file mode 100644 index 00000000..0bd5f2c3 --- /dev/null +++ b/sglang/CVE-2026-3059/README.md @@ -0,0 +1,112 @@ +# CVE-2026-3059: SGLang Unauthenticated ZeroMQ Broker Deserialization + +SGLang versions `0.5.5` through `0.5.9` contain an unauthenticated deserialization vulnerability (CWE-502) in `run_zeromq_broker()` within the multimodal generation runtime (`python/sglang/multimodal_gen/runtime/scheduler_client.py`). When the SGLang HTTP server starts on `--port

` (default `30000`), `run_zeromq_broker()` binds an unauthenticated ZeroMQ `ROUTER` socket to `tcp://*:

` (default `30001`) and processes incoming multipart frames directly using `pickle.loads()`. + +In SGLang `0.5.10` ([PR #21435](https://github.com/sgl-project/sglang/pull/21435)), `run_zeromq_broker()` was patched to bind strictly to the local loopback interface (`tcp://127.0.0.1:

`). + +--- + +## Target Environment & Preconditions + +* **Affected Versions:** SGLang `0.5.5` – `0.5.9` (fixed in SGLang `0.5.10` via PR #21435) +* **Vulnerability Type:** CWE-502 (Deserialization of Untrusted Data) +* **Default Exposed Ports:** + * `30000` (HTTP) / `30001` (ZMQ Broker) — Vulnerable instance (`lmsysorg/sglang:v0.5.9-xeon`) + * `30010` (HTTP) — Patched instance (`lmsysorg/sglang:v0.5.10-xeon`, where ZMQ broker port `30011` is bound only to container loopback `127.0.0.1`) + +--- + +## Vulnerable Version + +### Setup + +Start the vulnerable SGLang container (`v0.5.9-xeon`) on HTTP port `30000` and ZeroMQ broker port `30001`: + +```sh +docker compose up -d sglang-vuln +``` + +### Testing the Vulnerability + +#### Step 1: Query SGLang Server Metadata (`GET /get_server_info`) + +```sh +curl -s http://127.0.0.1:30000/get_server_info | python3 -m json.tool +``` + +Response: + +```json +{ + "model_path": "Qwen/Qwen2-VL-7B-Instruct", + "tokenizer_path": "Qwen/Qwen2-VL-7B-Instruct", + "port": 30000, + "broker_port": 30001, + "version": "0.5.9" +} +``` + +#### Step 2: Verify Exposed Unauthenticated ZeroMQ Broker Handshake on Port `30001` + +```sh +python3 verify_zmtp.py +``` + +Response (Port `30001` accepts the connection and returns a 64-byte ZMTP 3.x greeting with `mechanism=NULL`): + +```text +[Port 30001] Connected! Received 64-byte ZMTP 3.x greeting (mechanism=NULL, header=ff00000000000000017f0301) +``` + +--- + +## Safe Version + +### Setup + +Start the patched SGLang container (`v0.5.10-xeon`) on HTTP port `30010` to confirm remediation: + +```sh +docker compose up -d sglang-patched +``` + +### Testing the Vulnerability + +#### Step 1: Query SGLang Server Metadata (`GET /get_server_info`) + +```sh +curl -s http://127.0.0.1:30010/get_server_info | python3 -m json.tool +``` + +Response: + +```json +{ + "model_path": "Qwen/Qwen2-VL-7B-Instruct", + "tokenizer_path": "Qwen/Qwen2-VL-7B-Instruct", + "port": 30010, + "broker_port": 30011, + "version": "0.5.10" +} +``` + +#### Step 2: Verify ZeroMQ Broker Port `30011` Is Not Externally Reachable + +```sh +python3 verify_zmtp.py +``` + +Response (Port `30011` is bound to `127.0.0.1` inside `v0.5.10` and refuses external connections): + +```text +[Port 30011] Connection refused / closed ([Errno 111] Connection refused) +``` + +--- + +## Cleanup + +```sh +docker compose down +``` + diff --git a/sglang/CVE-2026-3059/docker-compose.yml b/sglang/CVE-2026-3059/docker-compose.yml new file mode 100644 index 00000000..ac146d01 --- /dev/null +++ b/sglang/CVE-2026-3059/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.8' + +services: + sglang-vuln: + image: docker.io/lmsysorg/sglang:v0.5.9-xeon + container_name: cve-2026-3059-vuln + ports: + - "30000:30000" + - "30001:30001" + environment: + - PYTHONUNBUFFERED=1 + - SGLANG_PORT=30000 + volumes: + - ./sglang_lab_server.py:/app/sglang_lab_server.py:ro + command: ["/opt/.venv/bin/python3", "/app/sglang_lab_server.py"] + restart: unless-stopped + + sglang-patched: + image: docker.io/lmsysorg/sglang:v0.5.10-xeon + container_name: cve-2026-3059-patched + ports: + - "30010:30010" + environment: + - PYTHONUNBUFFERED=1 + - SGLANG_PORT=30010 + volumes: + - ./sglang_lab_server.py:/app/sglang_lab_server.py:ro + command: ["/opt/.venv/bin/python3", "/app/sglang_lab_server.py"] + restart: unless-stopped + diff --git a/sglang/CVE-2026-3059/sglang_lab_server.py b/sglang/CVE-2026-3059/sglang_lab_server.py new file mode 100644 index 00000000..34bd072a --- /dev/null +++ b/sglang/CVE-2026-3059/sglang_lab_server.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""CPU launcher for SGLang CVE-2026-3059 inside lmsysorg/sglang:v0.5.9-xeon and v0.5.10-xeon.""" + +import asyncio +import importlib.util +import json +import logging +import os +import sys +import threading +import types +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +import sglang.version + +logging.basicConfig(level=logging.INFO) +port = int(os.environ.get("SGLANG_PORT", "30000")) +ver = sglang.version.__version__ + +# 1. Load and start SGLang's real run_zeromq_broker() from the container's installation +sys.modules["sglang.multimodal_gen"] = types.ModuleType("sglang.multimodal_gen") +sys.modules["sglang.multimodal_gen.runtime"] = types.ModuleType("sglang.multimodal_gen.runtime") +sys.modules["sglang.multimodal_gen.runtime.server_args"] = types.SimpleNamespace(ServerArgs=object) +sys.modules["sglang.multimodal_gen.runtime.utils"] = types.ModuleType("sglang.multimodal_gen.runtime.utils") +sys.modules["sglang.multimodal_gen.runtime.utils.logging_utils"] = types.SimpleNamespace( + init_logger=logging.getLogger +) + +spec = importlib.util.spec_from_file_location( + "scheduler_client", + "/sgl-workspace/sglang/python/sglang/multimodal_gen/runtime/scheduler_client.py", +) +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) + +threading.Thread( + target=lambda: asyncio.run( + mod.run_zeromq_broker(types.SimpleNamespace(broker_port=port + 1)) + ), + daemon=True, +).start() + + +# 2. Serve SGLang's HTTP metadata endpoints (/get_server_info, /get_model_info, /health) +class Handler(BaseHTTPRequestHandler): + def log_message(self, fmt, *args): + pass + + def do_GET(self): + routes = { + "/health": {"status": "ok"}, + "/get_server_info": { + "model_path": "Qwen/Qwen2-VL-7B-Instruct", + "tokenizer_path": "Qwen/Qwen2-VL-7B-Instruct", + "port": port, + "broker_port": port + 1, + "version": ver, + }, + "/get_model_info": { + "model_path": "Qwen/Qwen2-VL-7B-Instruct", + "tokenizer_path": "Qwen/Qwen2-VL-7B-Instruct", + "is_generation": True, + }, + } + payload = routes.get(self.path) + body = json.dumps(payload or {"detail": "Not Found"}).encode() + self.send_response(200 if payload else 404) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + +if __name__ == "__main__": + print(f"SGLang v{ver} listening on HTTP port {port} (broker_port={port + 1})", flush=True) + ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever() + diff --git a/sglang/CVE-2026-3059/verify_zmtp.py b/sglang/CVE-2026-3059/verify_zmtp.py new file mode 100644 index 00000000..0332ae91 --- /dev/null +++ b/sglang/CVE-2026-3059/verify_zmtp.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +import socket + +ZMTP_GREETING = ( + b"\xff\x00\x00\x00\x00\x00\x00\x00\x01\x7f" + b"\x03\x00" + b"NULL" + b"\x00" * 16 + + b"\x00" * 32 +) + +for port in (30001, 30011): + try: + with socket.create_connection(("127.0.0.1", port), timeout=2.0) as s: + s.sendall(ZMTP_GREETING) + resp = b"" + while len(resp) < 64: + chunk = s.recv(64 - len(resp)) + if not chunk: + break + resp += chunk + mech = resp[12:17].rstrip(b"\x00").decode("ascii", errors="replace") + print(f"[Port {port}] Connected! Received {len(resp)}-byte ZMTP 3.x greeting (mechanism={mech}, header={resp[:12].hex()})") + except Exception as e: + print(f"[Port {port}] Connection refused / closed ({e})") + From 8e6bf948b9890ffe509f29f673e63cff80fda622 Mon Sep 17 00:00:00 2001 From: MANGESH NAGNATWALSANE Date: Thu, 15 Oct 2026 06:46:45 +0000 Subject: [PATCH 2/2] Fixed review comment in sglang_lab_server.py --- sglang/CVE-2026-3059/sglang_lab_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sglang/CVE-2026-3059/sglang_lab_server.py b/sglang/CVE-2026-3059/sglang_lab_server.py index 34bd072a..439b77a1 100644 --- a/sglang/CVE-2026-3059/sglang_lab_server.py +++ b/sglang/CVE-2026-3059/sglang_lab_server.py @@ -30,6 +30,8 @@ "scheduler_client", "/sgl-workspace/sglang/python/sglang/multimodal_gen/runtime/scheduler_client.py", ) +if spec is None or spec.loader is None: + raise ImportError("Could not load spec or loader for scheduler_client.py") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod)