Key points of this blog
- VicOne researcher Reuel Magistrado discovered CVE-2026-86793, a vulnerability that allows permitted Python builtins functions to bypass SGLang's
SafeUnpicklerrestrictions. - Through the
/update_weights_from_tensorendpoint, this bypass could lead to remote code execution when authentication is not configured and the SGLang server is accessible to an attacker. - Pending a confirmed vendor patch, recommended measures include configuring authentication, restricting access to the endpoint, and reviewing the researcher's proposed code-level remediation.
SGLang is an open-source inference framework used to deploy large language models (LLMs). During a recent security review of its codebase, VicOne researcher Reuel Magistrado discovered a critical vulnerability, assigned CVE-2026-86793, that could allow a remote, unauthenticated attacker to execute arbitrary code on an affected SGLang deployment.
The vulnerability resides in SGLang's custom SafeUnpickler implementation. Although the unpickler was introduced to mitigate earlier deserialization attacks associated with CVE-2025-10164, its broad allowlist for the Python builtins module and incomplete denylist allow restrictions to be bypassed through an __import__ and getattr gadget chain.
![]()
Figure 1. Conceptual diagram illustrating how permitted builtins functions can be chained to bypass SafeUnpickler restrictions and execute arbitrary code
This post details the vulnerability, the flawed code path, and a technical analysis of the full code flow from the unauthenticated HTTP endpoint to the final gadget chain execution.
How SGLang restricts pickle deserialization
Pickle deserialization is a well-known attack surface in Python machine learning ecosystems. SGLang was previously found to be vulnerable to arbitrary code execution through unsafe pickle loading, tracked as CVE-2025-10164.
To address this vulnerability, SGLang introduced a custom unpickler class, SafeUnpickler, located in python/sglang/srt/utils/common.py. The class overrides the find_class() method to restrict which Python modules, classes, and functions can be loaded during deserialization. It relies on two mechanisms:
ALLOWED_MODULE_PREFIXES: A list of safe module prefixes, such as"torch."and"numpy.".DENY_CLASSES: A list of specific module-and-name pairs that must be blocked, such as("builtins", "eval").
How pickle deserialization can lead to code execution
Python's pickle module is used to serialize and deserialize object hierarchies. When deserializing an object, pickle may import modules and resolve the classes or functions needed to reconstruct it through the find_class() method.
If an attacker can control the byte stream being deserialized, they can manipulate these reconstruction instructions to call a function with attacker-controlled arguments. If the unpickler fails to block a dangerous function, the attacker's command may be executed as the payload is deserialized.
SGLang's SafeUnpickler attempts to prevent this behavior by restricting which functions can be resolved. However, the interaction between its allowlist and denylist leaves a path through which these restrictions can be bypassed.
The vulnerability: flawed allowlist and denylist logic
The vulnerability results from how the allowlist and denylist interact.
Problem 1: The builtins prefix is too broad
ALLOWED_MODULE_PREFIXES includes the broad prefix "builtins.". Because of how prefix matching works in find_class(), any name from the builtins module is permitted unless it is explicitly included in DENY_CLASSES.
Problem 2: The denylist is incomplete
DENY_CLASSES blocks functions eval, exec, compile, and open, but it does not block __import__ or getattr.
The result
An attacker can use __import__ and getattr as a gadget chain to reach any function in any importable module — bypassing all intended restrictions.
An attacker can use __import__ and getattr as a gadget chain to reach any function in any importable module, including os.system, without ever triggering the denylist. The restrictions are bypassed not by breaking the rules, but by staying within them.
![]()
Figure 2. Simplified view of the SafeUnpickler logic relevant to CVE-2026-86793
Technical analysis: full code flow from endpoint to execution
To understand exactly how the vulnerability can be exploited, we trace the data flow from the network entry point to the execution of the gadget chain.
Step 1: The unauthenticated entry point
The attack begins at the /update_weights_from_tensor HTTP endpoint, defined in python/sglang/srt/entrypoints/http_server.py.
The endpoint uses AuthLevel.ADMIN_OPTIONAL. When neither an API key nor an admin API key is configured, it accepts requests without authentication.
An attacker who can reach the SGLang HTTP server could send a POST request containing a base64-encoded pickle payload in the serialized_named_tensors array.
![]()
Figure 3. The SGLang endpoint through which serialized tensor data enters the vulnerable code path.
Step 2: The deserialization trigger
After receiving the request, SGLang extracts the base64 string, decodes it into raw bytes, and passes it to the SafeUnpickler to load the "tensors."
When SafeUnpickler.load() begins parsing the pickle bytecode, a GLOBAL opcode instructs it to resolve a class or function. This triggers the custom find_class() method.
![]()
Figure 4. The attack flow from an unauthenticated HTTP request to deserialization and arbitrary code execution.
Step 3: The bypass execution chain
The malicious pickle payload can use standard pickle opcodes to chain permitted builtins functions. To understand the bypass, the following is a simplified Python logic that illustrates the process:
![]()
Figure 5. A Python representation of the gadget chain encoded in the pickle payload.
Here is how SafeUnpickler processes the opcodes for this chain:
Step A: Importing the os module
The payload uses a GLOBAL opcode to resolve builtins.__import__.
![]()
Figure 6. The GLOBAL opcode used to resolve builtins.__import__ during the first stage of the bypass.
Because "builtins." appears in ALLOWED_MODULE_PREFIXES and __import__ does not appear in DENY_CLASSES, SafeUnpickler permits it. A R (REDUCE) opcode then calls __import__("os"), placing the imported os module on the pickle stack.
Step B: Retrieving the system function
The payload then uses another GLOBAL opcode to resolve builtins.getattr.
![]()
Figure 7. The GLOBAL opcode used to resolve builtins.getattr during the second stage of the bypass.
This also passes the SafeUnpickler checks. A REDUCE opcode calls getattr(os_module, "system"), dynamically retrieving the os.system function.
Because the function is retrieved through getattr, SafeUnpickler does not directly process the blocked ("os", "system") pair.
Step C: Executing the command
The payload places a command string, such as "touch /tmp/poc_confirmed" on the stack and uses another REDUCE opcode to call the retrieved function: os.system("touch /tmp/poc_confirmed").
Because SafeUnpickler inspects only the module and name passed to find_class(), it never sees ("os", "system") being resolved directly. It sees only the permitted builtins functions used to retrieve os.system dynamically.
Remediation
As of this post's publication date, the SGLang maintainers had acknowledged the report but had not provided a patch or remediation timeline. The following measures were developed by VicOne researcher Reuel Magistrado and have not been validated as an official SGLang fix.
To fix this vulnerability, SafeUnpickler should abandon broad prefix matching for the builtins module. Instead, it should use an explicit class allowlist.
Minimal fix
Add __import__, getattr, setattr, and delattr to the DENY_CLASSES set.
Preferred fix
Remove "builtins." from ALLOWED_MODULE_PREFIXES entirely and use an explicit allowlist of exact classes required for tensor serialization:
![]()
Figure 8. Example of an explicit class allowlist proposed for safer tensor deserializationInsecure deserialization remains a significant threat in the AI and ML software ecosystem. In CVE-2026-86793, the broad allowlist for Python’s built module and an incomplete denylist allow permitted functions to be chained to execute arbitrary code.
Conclusion
CVE-2026-86793 illustrates a broader principle: secure deserialization cannot rely only on blocking known dangerous functions. AI inference frameworks should use narrowly scoped allowlists for serialized data and require authentication for administrative endpoints.
Until an official patch becomes available, organizations using SGLang should enable authentication and restrict access to the affected endpoint to trusted networks.
Disclosure timeline
- June 29, 2026: Reuel Magistrado submitted a private GitHub Security Advisory (GHSA) to the SGLang maintainers and sent a follow-up email to the project's maintainer addresses.
- July 2, 2026: A maintainer acknowledged the report through Slack but did not provide a patch or remediation timeline.
- July 16, 2026: The vulnerability was escalated to CERT/CC for coordinated disclosure due to the maintainer's lack of response.
- July 30, 2026: CERT/CC validated the vulnerability.
- September 8, 2026: CERT/CC assigned CVE-2026-86793.
- September 11, 2026: VicOne published this technical analysis and provided its URL to CERT/CC for inclusion in the public CVE record.
Credits
- Reuel Magistrado, VicOne: Vulnerability discovery, private proof-of-concept (PoC) development, and coordinated disclosure
- Christopher Cullen and CERT/CC: Case coordination, vulnerability validation, and CVE assignment
References
- CVE ID: CVE-2026-86793
- Coordinator: CERT/CC
- Vendor: sgl-project / LMSYS
- Affected Versions: SGLang <= 0.5.14