Repository navigation
Added testbed for SGLang CVE-2026-3059 #277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mangeshwalsane2-hash
wants to merge
2
commits into
google:main
Choose a base branch
from
mangeshwalsane2-hash:testbeds/CVE-2026-3059-sglang
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <P>` (default `30000`), `run_zeromq_broker()` binds an unauthenticated ZeroMQ `ROUTER` socket to `tcp://*:<P + 1>` (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:<P + 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 | ||
| ``` | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| #!/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", | ||
| ) | ||
| 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) | ||
|
|
||
| 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() | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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})") | ||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
importlib.util.spec_from_file_locationfunction can returnNoneif the file cannot be found or the spec cannot be created. Directly passingspectoimportlib.util.module_from_specwithout checking if it isNoneor ifspec.loaderisNonewill raise a crypticValueErrororAttributeError. Adding a defensive check ensures a clear and actionable error message is raised if the file is missing or inaccessible.