Introduction
An unauthenticated remote code execution vulnerability in InternLM's LMDeploy toolkit lets anyone who can reach the server's /update_weights endpoint run arbitrary operating system commands by sending a crafted pickle payload. The default configuration binds to 0.0.0.0 and ships without API key authentication, so any internet-exposed LMDeploy instance running versions 0.9.0 through 0.18.0 is trivially exploitable.
LMDeploy is an open source toolkit for compressing, deploying, and serving large language models, developed by the MMRazor and MMDeploy teams under the InternLM project. It supports inference across model families including InternLM, Llama, Qwen, and DeepSeek, and provides a TurboMind backend (optimized for throughput) and a PyTorch backend (optimized for accessibility). As organizations deploy more LLM inference servers in production, the security of these serving frameworks becomes a direct concern for enterprise AI infrastructure.
Technical Information
Root Cause: Unsafe Pickle Deserialization (CWE-502)
The core issue is textbook CWE-502: the server deserializes attacker-controlled data with Python's pickle module and applies no validation or restriction. Python's pickle protocol is unsafe for untrusted input because it supports __reduce__ callbacks that can invoke arbitrary functions during object reconstruction.
The POST /update_weights endpoint accepts a JSON body containing a serialized_named_tensors field. This field accepts a string, a list of strings, or a dictionary. When a string is provided, the backend treats it as base64-encoded serialized data and processes it through the following call chain:
POST /update_weights
-> UpdateParamsRequest.serialized_named_tensors
-> api_server.update_params(...)
-> VariableInterface.async_engine.engine.update_params(request)
-> pybase64.b64decode(request.serialized_named_tensors)
-> ForkingPickler.loads(...)
-> native _pickle.loads(...) # attacker-controlled __reduce__ callbacks execute here
The ForkingPickler.loads call resolves to CPython's native _pickle.loads. At this point, any __reduce__ callback embedded in the pickle stream executes with the privileges of the api_server process. The base64 encoding step is only a transport representation and provides no security boundary.
Both Backends Are Affected
| Backend | Location | Security Consequence |
|---|---|---|
| TurboMind | lmdeploy/turbomind/turbomind.py, TurboMind.update_params | Deserializes request-controlled bytes using ForkingPickler.loads |
| PyTorch | lmdeploy/pytorch/engine/model_agent/agent.py, ModelAgent.update_params | Reaches the same unsafe deserialization operation |
| HTTP management layer | POST /update_weights via UpdateParamsRequest.serialized_named_tensors | Connects the network request to the backend operation |
Switching inference backends does not remove the exposure. Both paths converge on the same unsafe deserialization call.
Why Late Validation Does Not Help
The ordering of operations is the key detail. The pickle deserialization, and therefore any embedded code execution, happens before any tensor shape, structure, or weight validation. A later TypeError or validation failure does not undo the code that already ran during deserialization. The proof of concept below shows this directly.
Default Configuration Maximizes Exposure
The api_server defaults to binding on 0.0.0.0, and api_keys defaults to None. Authentication middleware is installed only when nonempty keys are explicitly configured. So a default deployment exposes the vulnerable management endpoint to the entire network with no authentication gate.
The VulnCheck CVSS 4.0 vector is CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N, scoring 9.3. Under CVSS 3.1, the score is 9.8. Both classify the vulnerability as critical.
Proof of Concept
A public proof of concept is available in GitHub issue #4698, filed against the InternLM/lmdeploy repository. The PoC (repro_update_weights_deser.py) is a local harness that stubs heavyweight CUDA and model dependencies, imports the real TurboMind implementation, and invokes TurboMind.update_params with a crafted pickle payload.
The payload uses a simple Marker class with a __reduce__ method that writes a file to disk:
class Marker: def __reduce__(self): return (out.write_text, ("target update_params deserialized payload\n",))
The harness creates a base64-encoded pickle of this object, constructs a mock request with it in serialized_named_tensors, and calls the real update_params method:
payload = base64.b64encode(pickle.dumps(Marker())).decode() request = types.SimpleNamespace(serialized_named_tensors=payload, finished=False) self_obj = types.SimpleNamespace(devices=[0]) print("loads_module", getattr(pickle.loads, "__module__", None)) print("marker_before", out.exists()) try: target.TurboMind.update_params(self_obj, request) except Exception as exc: print("target_exception_after_loads", type(exc).__name__, str(exc)) print("marker_after", out.exists()) if out.exists(): print("marker_text", out.read_text().strip())
The output confirms that the __reduce__ callback fires during ForkingPickler.loads() before any later validation:
loads_module _pickle marker_before False target_exception_after_loads TypeError 'int' object is not iterable marker_after True marker_text target update_params deserialized payload
The marker file is created before the TypeError, which is an expected failure because the harness uses a marker object rather than a real weight iterator. This demonstrates that arbitrary code execution occurs before tensor validation.
An end-to-end HTTP exploit against a live lmdeploy serve api_server instance would send a POST /update_weights request with the malicious base64-encoded pickle in serialized_named_tensors, reaching the same _pickle.loads sink without authentication.
The full PoC script is available in issue #4698.
Patch Information
A patch has been proposed via PR #4964. As of November 3, 2026, this PR remains open and has not been merged, and no patched release has been published. The PR is labeled Bug:P1 and modifies 9 files across 622 additions and 44 deletions.
What the Patch Does
The core fix inserts a guard at the top of the HTTP endpoint handler in lmdeploy/serve/openai/endpoints/management.py:
if (is_pickle_serialized_named_tensors(request.serialized_named_tensors, request.load_format) and not allow_pickle_update_params()): return create_error_response(HTTPStatus.BAD_REQUEST, _PICKLE_HTTP_ERROR)
This rejects any incoming request whose serialized_named_tensors is a base64 string (the wire format serialize_state_dict() uses for pickle blobs) with HTTP 400 before it ever reaches pickle.loads.
Several new utility functions are introduced in lmdeploy/utils.py:
is_pickle_serialized_named_tensors(payload, load_format)detects pickle blobs by checking whether the payload is astrorlist[str]. Ifload_format='safetensors', it returnsFalseto prevent false positives.allow_pickle_update_params()reads the environment variableLMDEPLOY_ALLOW_PICKLE_UPDATE_PARAMSand returnsTrueonly when it is explicitly set to'1', acting as a deliberate opt-in for trusted same-node XTuner CUDA IPC scenarios.load_pickled_serialized_named_tensors(serialized_data)is the only codepath that callsForkingPickler.loads. It checksallow_pickle_update_params()first and raises aValueErrorif the opt-in is not set, providing defense in depth.
Safe Serialization Alternatives
The patch introduces two safe alternatives:
- Safetensors path:
serialize_named_tensors_safetensors()andload_safetensors_serialized_named_tensors()use thesafetensorslibrary, a memory-safe binary format that cannot execute arbitrary code during deserialization. Clients setload_format='safetensors'on the request. - Structured dict path:
coerce_update_params_tensor(value)accepts JSON-safe tensor specs ({dtype, shape, data}) and explicitly rejects callables and pickle reduce tuples.
Both the TurboMind and PyTorch backends have their _deserialize_weights methods rewritten. The direct from multiprocessing.reduction import ForkingPickler and import pybase64 imports are removed from both backends.
Breaking Change
This is a breaking change. Existing clients that send pickle-encoded weights via serialize_state_dict() to /update_weights will receive HTTP 400 unless the server operator explicitly sets LMDEPLOY_ALLOW_PICKLE_UPDATE_PARAMS=1. The recommended migration is to switch to serialize_named_tensors_safetensors() with load_format='safetensors', or to use alternative endpoints like /update_weights_from_distributed (NCCL) or /update_weights_from_ipc (checkpoint engine).
Compatibility Opt-In Restores Risk
Enabling LMDEPLOY_ALLOW_PICKLE_UPDATE_PARAMS=1 deliberately restores pickle support. The LMDEPLOY_ALLOW_PICKLE_UPDATE_PARAMS variable is registered in lmdeploy/pytorch/envs.py so that Ray's get_all_envs() automatically copies it to worker processes. This is not a harmless compatibility switch. It is an explicit restoration of trusted pickle functionality.
Detection Methods
As of this writing, no formal detection signatures (Sigma rules, YARA rules, Snort/Suricata IDS rules, or Nuclei templates) have been published for CVE-2026-119706. OffSeq's Threat Radar tracker confirms that no public Nuclei template or exploit code has been indexed. Detection at this stage is behavioral and asset-inventory driven.
Network Traffic Monitoring
The primary detection signal is an HTTP POST request to the /update_weights endpoint on an LMDeploy api_server instance. TheHackerWire's advisory recommends that SOC analysts monitor for HTTP POST requests to this endpoint and flag those containing unusually large or malformed base64-encoded strings in the serialized_named_tensors parameter. Legitimate weight update payloads have a characteristic size and structure; crafted pickle objects built for code execution (embedding __reduce__ callbacks) tend to show different entropy and size profiles. Analysts should examine both the length and the Shannon entropy of the base64-encoded values.
Host-Based Process Monitoring
Successful exploitation results in arbitrary command execution under the api_server process. Configure process-tree auditing (Linux auditd or EDR agent telemetry) on hosts running LMDeploy and alert when the Python process hosting api_server forks a shell (sh, bash, cmd.exe) or runs command-line tools that have no reason to appear in an inference pipeline.
Access Log Review
IONIX's advisory urges organizations to review exposed LMDeploy servers for unexpected requests to /update_weights and for signs of compromise. TheHackerWire recommends reviewing access logs for requests to this endpoint that originate from suspicious IP ranges or that arrive at high frequency. Any external or unexpected request to this management endpoint on a production inference server warrants investigation.
Attack Surface Discovery
TheHackerWire suggests the Shodan dork http.title:"LMDeploy API Server" "/update_weights" to locate potentially vulnerable internet-facing servers. This is a useful proactive reconnaissance step for teams managing AI/ML infrastructure.
Version-Based Detection
All versions of InternLM LMDeploy from 0.9.0 through 0.18.0 (inclusive) are affected. The PyPI package name is lmdeploy. Asset inventory tools and software composition analysis (SCA) scanners should flag any installed package matching pkg:pypi/lmdeploy within the affected version range.
Affected Systems and Versions
- Software: InternLM LMDeploy
- Affected versions: 0.9.0 through 0.18.0 (inclusive)
- Package:
pkg:pypi/lmdeploy - Affected backends: Both TurboMind and PyTorch inference backends
- Vulnerable endpoint:
POST /update_weights - Vulnerable configuration: The default configuration (binds to
0.0.0.0, no API keys configured) maximizes exposure, but any reachable instance running an affected version is vulnerable regardless of backend choice - Fixed version: None confirmed. PR #4964 remains open and unmerged as of November 3, 2026.
Vendor Security History
LMDeploy has a documented pattern of unsafe deserialization vulnerabilities across several attack surfaces:
| CVE | Mechanism | Affected Versions | Patched Version |
|---|---|---|---|
| CVE-2026-119706 | HTTP weight update pickle deserialization | 0.9.0 through 0.18.0 | No fixed release confirmed |
| CVE-2025-59953 | Unsafe pickle deserialization in the ZMQ RPC server | At least 0.9.1 and below 0.10.2 | 0.10.2 |
| CVE-2025-66455 | DistServe P2P connection causes deserialization from an attacker-controlled ZeroMQ peer | At least 0.9.2 and below 0.16.0 | 0.16.0 |
| CVE-2025-67729 | Model checkpoint loading through unsafe torch.load() | At or below 0.11 | None listed |
| CVE-2026-33626 | SSRF through vision-language image loading | At or below 0.12.0 | None listed |
The recurring unsafe deserialization theme is notable, but each vulnerability involves a distinct mechanism, entry point, and remediation. The fixes for CVE-2025-59953 (version 0.10.2) and CVE-2025-66455 (version 0.16.0) do not address CVE-2026-119706. The DistServe P2P fix replaced Python object messaging with JSON messaging on a different endpoint (/distserve/p2p_connect), and the checkpoint vulnerability requires a victim to load a malicious model file, unlike the unauthenticated network request described here.
References
- NVD: CVE-2026-119706
- VulnCheck Advisory: LMDeploy 0.9.0 through 0.18.0 Unauthenticated RCE via /update_weights Pickle Deserialization
- GitHub Issue #4698: /update_weights deserializes request-controlled pickle data before validation
- GitHub PR #4964: Reject pickle on HTTP /update_weights
- GitHub PR #4964 API
- GitHub Issue #4698 API
- PR #4964 Diff
- Vulnerable Source: management.py (v0.18.0)
- Vulnerable Source: turbomind.py (v0.18.0)
- Vulnerable Source: management.py endpoints (v0.18.0)
- InternLM/lmdeploy GitHub Repository
- IONIX Threat Center: CVE-2026-119706
- TheHackerWire: CVE-2026-119706 Analysis
- TheHackerWire: CVE-2026-119706 Detection Guidance
- OffSeq Threat Radar: CVE-2026-119706
- LMDeploy Releases
- GHSA-5h8j-6crg-7rmw: ZMQ RPC Server Pickle Deserialization
- GHSA-2vh9-42vm-xmv2: DistServe P2P Pickle Deserialization
- GHSA-9pf3-7rrr-x5jh: torch.load() Arbitrary Code Execution
- GHSA-6w67-hwm5-92mq: SSRF in Vision-Language Image Loading



