Six vulnerabilities were the visible result of this research. The deeper finding was architectural. SGLang defaults to a loopback binding but is routinely exposed through Docker, Kubernetes, and cloud GPU platforms. In that exposed state, public inference, administrative control, model loading, and distributed weight management shared boundaries that were optional, incomplete, or absent.

The Origin Story

I didn’t start this review expecting to write about six CVEs.

The early findings looked unrelated. One was an eval() sandbox escape in a debugging subsystem. Another was an SSRF primitive in multimodal media loading. A third was a supposedly safe unpickler. Each came with its own endpoint, its own exploit conditions, and its own obvious fix.

But one question kept surfacing: why could untrusted input reach this operation in the first place?

Chasing that question outward reshaped the whole investigation. These vulnerable operations weren’t isolated helpers. They sat behind HTTP routes on an inference server that could also load models, expose configuration, spin up distributed process groups, and move model weights around. Several of those routes relied on an authentication mode that was designed to fall open when operators didn’t configure keys. Others were assigned a weaker authentication level than the sensitivity of the data they handed back.

That’s how six separate vulnerability reports collapsed into a single architectural story.

I disclosed the findings through GitHub Security Advisories starting on April 22, 2026. When the initial coordination channel went quiet, I brought in CERT/CC to coordinate the case. CERT/CC assigned VU#281278 and published a vulnerability note covering all six CVEs.

This post walks through the root causes, the exploit paths, what was verified, and where the evidence runs out.


What I Found

The six findings span code execution, server-side request forgery, credential disclosure, and model confidentiality. Their proof levels are not identical, so the table separates live validation from source-confirmed behavior.

Finding Identifier Vulnerability Primary impact Required condition
1 CVE-2026-15969 SafeUnpickler denylist bypass Remote code execution Reachable tensor update route; no keys in the tested configuration
2 CVE-2026-15971 Dumper eval() sandbox escape Remote code execution Dumper HTTP control explicitly enabled
3 CVE-2026-15974 Multimodal SSRF and local file access Internal service access; file existence oracle; media disclosure Reachable multimodal inference route
4 CVE-2026-15976 weights_only=False model-loading fallback Code execution during model loading Reachable model update route and a malicious legacy checkpoint
5 CVE-2026-15977 /server_info credential disclosure Admin-key theft and privilege escalation Admin-key-only or split user/admin key configuration
6 CVE-2026-15978 Untrusted distributed peer enrollment Model weight disclosure Reachable control route, outbound connectivity, compatible receiver

Those six findings collapse into three root causes: optional authentication, executable data formats, and unconstrained server-side fetching. The next sections walk through each root cause, then map how the findings chain together.


The Architecture: One Port, Multiple Trust Domains

An inference server looks simple from the outside: send a prompt, receive tokens. Internally, it is a control plane for expensive and sensitive resources.

SGLang’s HTTP service includes at least four distinct trust domains:

Plane Typical operation Asset at risk
Inference Generate text; process images, audio, and video Prompts, media, internal network reachability
Administration Inspect configuration; flush caches; control runtime behavior API keys, TLS secrets, service availability
Model loading Load or replace checkpoints and adapters Code execution boundary, model integrity
Distributed control Create process groups; send or receive tensors Proprietary model weights, cluster membership

These operations do not carry the same risk. A caller authorized to submit a prompt should not automatically be authorized to replace model weights. A diagnostics endpoint should not serialize the credentials that protect administrative routes. A remote peer should not become a trusted cluster member because it supplied an address in an HTTP request.

The route map is where the trust-boundary problem becomes visible. These planes shared the same service and, in several cases, the same optional authentication policy.

SGLang route trust boundary map

Two routes had no authentication decorator at all: /server_info and /load_lora_adapter_from_tensors. They fell through to the default inference-level policy regardless of key configuration. The remaining administrative routes used ADMIN_OPTIONAL, which falls open when no keys are configured.

What ADMIN_OPTIONAL Means

The relevant behavior can be reduced to this decision tree:

if endpoint_is_admin_optional:
    if admin_api_key_is_configured:
        require_admin_api_key()
    elif api_key_is_configured:
        require_api_key()
    else:
        allow_request()

That last branch is the vulnerability amplifier. It does not create every sink described in this post, but it converts high-impact management operations into unauthenticated routes whenever no key is configured.

The network context matters. SGLang defaults to a loopback binding in the versions I reviewed. Network exploitation therefore requires an operator, container platform, reverse proxy, or orchestration layer to make the service reachable. That is common in Docker and Kubernetes deployments, but it is not the same as claiming every default installation is internet-exposed.

Authentication behavior also changes with key configuration. The dangerous cases are the last two rows: when only an admin key is set, or when both keys are set, /server_info discloses the admin key to unauthenticated or low-privilege callers.

Configuration Normal inference routes ADMIN_OPTIONAL routes /server_info consequence
No keys Public Public Configuration exposed; no configured keys to leak
--api-key only API key required API key required API key reflected to its holder
--admin-api-key only Public Admin key required Admin key disclosed without authentication
Both keys API key required Admin key required Ordinary API user receives the admin key

This matrix led to the first systemic root cause.


Root Cause 1: Optional Authentication Is Not a Boundary

Credential Disclosure Through /server_info

The /server_info route returned a dictionary built from the complete server configuration:

return {
    **dataclasses.asdict(server_args),
    **scheduler_info,
    "internal_states": internal_states,
    "version": version,
}

There were two defects in that short block.

First, the response used broad serialization where an explicit public response schema was required. Fields included api_key, admin_api_key, and ssl_keyfile_password alongside routine configuration.

Second, the route had no @auth_level decorator, so the middleware defaulted it to AuthLevel.NORMAL. The normal policy checked --api-key when present but did not treat --admin-api-key as a reason to restrict the route. This is distinct from the ADMIN_OPTIONAL defect: even if ADMIN_OPTIONAL were fixed, this route would have remained unprotected.

I tested both dangerous configurations on SGLang 0.5.10.post1:

  • With only --admin-api-key, an unauthenticated request returned the admin key in plaintext.
  • With both keys, a caller holding the ordinary API key received the admin key and could use it on administrative routes.

The deprecated /get_server_info alias behaved the same way. This was not merely a documentation ambiguity: SGLang’s own documentation described /server_info as an admin-protected endpoint, while the implementation did not enforce that boundary.

From a Leaked Key to Distributed Control

SGLang also exposed endpoints for creating distributed weight-transfer groups and broadcasting model parameters. Request fields such as master_address, ports, rank, world size, group name, and backend crossed directly from HTTP into distributed initialization.

The send path then iterated through model parameters and broadcast each tensor to members of the group:

for _, weights in model.named_parameters():
    torch.distributed.broadcast(
        weights,
        src=0,
        group=send_group,
    )

The trust error is straightforward: a caller-selected peer was treated as a cluster member without an independent identity or membership check.

I validated that the affected routes were reachable without credentials when no keys were configured. I also validated the credential chain: when an admin key protected those routes, /server_info disclosed the key, and the same requests then passed authentication.

The reverse update path also suggests model-weight injection, but rank and tensor acceptance behavior were not validated to the same standard. Weight injection remains an unverified variant.

The Meta-Finding

An inference credential, an administrative credential, and cluster membership are different security concepts. They require separate authorization policies and, ideally, separate network surfaces. An endpoint that moves model weights should not become public because an operator omitted a command-line key.


Root Cause 2: Untrusted Data Becomes Python Execution

Three findings reached code execution through different features. All three preserved Python’s ability to execute behavior supplied by data.

A “Safe” Unpickler With a Universal Escape Hatch

SGLang introduced SafeUnpickler to remediate CVE-2025-10164, an earlier pickle deserialization vulnerability. This bypass renders that fix incomplete. The implementation combined broad module-prefix allowlisting with a denylist of individual dangerous functions.

In simplified form:

if (module, name) in denied_classes:
    reject()
if module_name_starts_with_allowed_prefix(module):
    return imported_attribute(module, name)
reject()

The allowlist included the entire builtins namespace. The denylist blocked obvious functions such as eval, exec, and compile, but allowed the two capabilities needed to recover arbitrary functionality: dynamic import and attribute lookup.

The exploit chain therefore did not call a denied function directly. It used one permitted builtin to obtain a module and another to select a command-execution primitive from that module. The resulting pickle payload passed SafeUnpickler.find_class() and executed during deserialization.

I delivered the payload to /update_weights_from_tensor (which uses ADMIN_OPTIONAL authentication) on SGLang 0.5.10.post1 without authentication in the tested no-key configuration. The route returned HTTP 200, the marker command ran as the SGLang process user, and the server remained healthy. A second entry point, /load_lora_adapter_from_tensors, has no authentication decorator at all and is unauthenticated regardless of key configuration when --enable-lora is active.

The demonstrated payload used only builtins functions, but it illustrates a more general problem. Adding __import__ and getattr to the denylist would remove this specific payload without closing the class: dynamic import and attribute lookup are not the only route to arbitrary behavior. If the allowlist extends beyond builtins to utility modules such as operator or functools, callables like operator.attrgetter or functools.partial reconstruct the same capability through a parallel allowed path. Any denylist-only approach remains a game of whack-a-mole against a fundamentally unsafe design.

Compatibility That Disables weights_only=True

The second deserialization issue appeared in model loading. PyTorch supports weights_only=True to restrict the objects reconstructed from a checkpoint. SGLang tried the safe mode first, then deliberately disabled it for legacy tar-format files:

try:
    return torch.load(checkpoint, weights_only=True)
except RuntimeError as error:
    if "legacy .tar format" in str(error):
        return torch.load(checkpoint, weights_only=False)
    raise

This is a security fallback in the wrong direction. A file that the restricted loader rejects receives a second attempt with the dangerous behavior restored.

I validated the deserialization primitive and the HTTP path using a local legacy-format checkpoint. The marker payload executed before model architecture or tensor-shape validation. I also confirmed from source that a non-local model_path can enter the HuggingFace download path and that a repository containing only .bin files selects the unsafe loader.

The complete remote HuggingFace delivery was not demonstrated to the same level as the local HTTP-to-deserialization path. It also depends on outbound connectivity and on the malicious artifact remaining available despite repository scanning. The claim should therefore remain precise: the execution primitive is live-validated; remote Hub delivery is a source-confirmed chain with environmental preconditions.

eval() Is Not a Filter Language

The dumper subsystem accepted a user-controlled filter expression and evaluated it with Python:

namespace = tags_with_filter_helpers(tags)
return bool(eval(filter_expression, {"__builtins__": {}}, namespace))

Removing __builtins__ does not create a Python sandbox. Literal objects still expose their classes, base classes, and the runtime subclass graph. From there, an expression can recover references to modules and process-execution functions without naming a builtin directly.

The result being wrapped in bool() only makes the vulnerability blind. Side effects still occur.

This finding has an important deployment qualifier: the dumper HTTP control is not enabled by default. Exploitation requires DUMPER_SERVER_PORT to be set. In standalone mode, the dumper used a separate unauthenticated HTTP server bound to all interfaces. In reuse mode, it inherited the main server’s optional administrative policy.

The Meta-Finding

Pickle, unrestricted model checkpoints, and eval() are dangerous because each asks Python to reconstruct behavior from untrusted data. Denylists and empty builtin dictionaries constrain that behavior while preserving its execution semantics. Durable remediation removes the executable format from the trust boundary entirely.


Root Cause 3: A URL Is a Capability

SGLang’s OpenAI-compatible multimodal input accepted URLs for images, audio, and video. The media helpers treated those strings as instructions for the server:

if source_is_local_path(media_url):
    return read_file(media_url)
if source_is_http_url(media_url):
    return requests.get(media_url, timeout=timeout).content

There was no destination allowlist, no private-address rejection, and no consistent prohibition on file:// or bare absolute paths.

That creates two capabilities:

  1. Network capability: the caller can make the SGLang host issue HTTP requests into networks the caller may not reach directly.
  2. Filesystem capability: the caller can make the SGLang process open local paths using its own operating-system permissions.

For HTTP targets returning valid media, the model can decode and describe the response, turning SSRF into a data-return channel. For ordinary local text files, the media decoder usually fails, but the error distinguishes an existing readable file from a missing or inaccessible one. Valid local media remains in scope for disclosure through model inference when the loader can decode it.

The limitations matter. In the tested request path for CVE-2026-15974, I could not add arbitrary metadata headers. GCP metadata requires Metadata-Flavor: Google; Azure IMDS requires its own metadata header; AWS IMDSv2 requires a token workflow.

The correct root-cause description is therefore SSRF plus unsafe local file access.


How the Findings Chain

Individual vulnerabilities are usually scored in isolation. Operators experience systems, not scorecards.

Chain 1: Configuration Disclosure to Distributed Weight Access

Reach /server_info
  -> obtain admin_api_key
  -> authenticate to distributed weight routes
  -> enroll caller-selected peer
  -> invoke model parameter broadcast

The first two transitions were live-validated. Source analysis confirmed the peer-enrollment and broadcast code paths. End-to-end receipt of every tensor on an external NCCL receiver was not completed during validation.

Chain 2: Configuration Disclosure to Unsafe Model Loading

Reach /server_info
  -> obtain admin_api_key
  -> authenticate to /update_weights_from_disk
  -> select attacker-controlled legacy checkpoint
  -> trigger weights_only=False deserialization

These chains show why “configure an admin key” was not a complete mitigation for the affected build. The endpoint that disclosed the key sat outside the administrative policy it was supposed to support.


What I Actually Proved

Security research loses value when source analysis, partial reproduction, and end-to-end exploitation are reported as if they were interchangeable. This is the evidence ledger for these findings:

Finding Demonstrated Source-confirmed Not demonstrated
CVE-2026-15969 Unauthenticated HTTP request executed a marker command on 0.5.10.post1 Additional allowlisted gadget families Every possible gadget or deployment mode
CVE-2026-15971 Malicious filter escapes the empty-builtins eval() context HTTP configuration and inference-trigger path Default exposure without enabling dumper
CVE-2026-15974 Server-side HTTP fetch, local file open, and differentiated errors Image, audio, and video share unsafe media semantics GCP/Azure token theft; arbitrary text-file response
CVE-2026-15976 HTTP-selected local legacy checkpoint executed during unsafe fallback Non-local identifiers reach Hub download and .bin selection Complete malicious-Hub-to-RCE chain in one run
CVE-2026-15977 Admin key leaked anonymously or to an ordinary API user Other sensitive fields are broadly serialized N/A for the primary credential leak
CVE-2026-15978 Routes passed or rejected authentication according to the vulnerable matrix Caller-selected peer enters a loop broadcasting all named parameters Complete external NCCL capture; weight injection

That distinction shaped both the disclosure language and the operator guidance below: live bugs should be patched, but source-confirmed management-plane paths should still be treated as reachable until the boundary is closed.


Responsible Disclosure

The coordination history:

Date Event
2026-04-22 I submitted the dumper RCE and multimodal SSRF findings through private GitHub Security Advisories.
2026-05-03 I submitted the SafeUnpickler, unsafe model-loading, credential-disclosure, and distributed-weight findings.
2026-05-06 I requested CERT/CC coordination after the initial disclosure channel did not produce a response.
2026-07-27 CERT/CC finalized the vulnerability note for VU#281278 and assigned six CVE identifiers.
2026-07-30 Published this companion post with CERT/CC VU#281278.

Fix the Class, Not Just the Payload

Each CVE needs a local patch. The architecture needs stronger invariants.

Separate the Management Plane

Inference and cluster administration should not share a default exposure boundary. Model loading, weight transfer, profiling, and runtime control should use a separate listener that binds privately by default. Deployments should be able to expose inference without exposing management routes.

Make Administrative Authentication Mandatory

Administrative routes should fail closed when no admin credential or workload identity is configured. A startup warning is useful, but refusing a public bind without administrative protection is stronger.

Authorization should also distinguish ordinary inference clients from operators. An inference API key should never authorize model replacement or distributed peer enrollment.

Serialize Public Responses Explicitly

Operational endpoints should return allowlisted response models. Serializing an internal configuration object and trying to redact known secrets later reverses the security model: every future secret field becomes public until somebody remembers to block it.

Authenticate Distributed Peers

A network address is not an identity. Weight-transfer channels need an operator-controlled peer allowlist, mutual authentication such as mTLS, and authorization bound to cluster membership. Egress policy should prevent model-management code from connecting to arbitrary public destinations.

Remove Executable Data Formats From Untrusted Paths

Network-provided tensors should use non-executable formats such as safetensors, with metadata represented in a constrained schema. Legacy pickle checkpoints should not receive an unsafe compatibility retry after restricted loading fails.

The dumper needs a small expression grammar or explicit filter operators, not Python eval() with a different namespace.

Treat URL Fetching as Delegated Authority

Media loaders need a protocol allowlist, operator-configured domain allowlists, redirect validation, DNS rebinding protection, and rejection of loopback, link-local, private, and otherwise restricted destinations. Local file access should require a separately configured media root, with canonical path enforcement.


If You Operate SGLang

Until fixed versions are identified, operators can reduce exposure:

  • Bind SGLang to loopback or a private service network; do not expose the native management surface directly to the internet.
  • Enforce route-level policy at a reverse proxy. Permit only required inference routes from untrusted networks.
  • Configure separate inference and administrative credentials where supported. Rotate all configured API keys and admin keys immediately if /server_info was ever reachable without appropriate authentication.
  • Block unnecessary outbound access from inference workers, especially arbitrary internet and cluster-management destinations.
  • Disable the dumper HTTP control, remote model updates, LoRA tensor loading, and distributed weight routes when unused.
  • Set SGLANG_USE_PICKLE_IPC=false where compatible with the deployment.
  • Prefer safetensors; reject untrusted .pt and .bin checkpoints and legacy tar formats.
  • Monitor requests to /server_info, /get_server_info, /update_weights_from_disk, tensor update routes, and distributed weight-group routes.
  • Review logs for unexpected model reloads, outbound distributed initialization, and access to media URLs on internal or link-local networks.

These controls reduce reachability. They do not replace patches for the individual vulnerable code paths.


The Bigger Picture

The most important mistake in AI infrastructure security is treating a model server as a stateless text API.

An inference runtime holds model weights, cloud credentials, HuggingFace tokens, TLS material, customer prompts, and access to GPU nodes. It can often download code-adjacent artifacts, join distributed groups, and replace a running model. That makes it a control plane with an unusually valuable data plane attached.

These patterns are visible in SGLang because I looked; they are likely present wherever inference infrastructure grew faster than its security architecture. The same shapes recur across other inference servers: the URL-as-capability shortcut, the pickle assumption, the default-open administrative route.

The fixes are not exotic. Fail closed. Separate inference from management. Use formats that cannot execute. Define public schemas explicitly. Authenticate peers as peers. Treat every server-side fetch as a grant of authority.

These findings brought those lessons into focus. The root causes are broader than any one framework.


What’s Next

This post is the map of the territory: the trust boundaries, the root causes, and what I actually proved. The next one will go closer to the metal.

I plan to write a technical follow-up on the exploit mechanics: the deserialization paths, the authentication edge cases, and the lab chains that made these issues real. The goal is reproducible understanding, not turnkey exploitation against live systems.


References

  1. CERT/CC VU#281278 https://www.kb.cert.org/vuls/id/281278. SGLang contains six different vulnerabilities including RCE, data exfiltration, and credential disclosure.

  2. GitHub Security Advisories

  3. PyTorch Serialization Semantics Serialization semantics: torch.load with weights_only=True

  4. Safetensors A simple, safe way to store and distribute tensors

  5. Python eval() Sandbox Background Eval really is dangerous


Apoorv Dayal is a security researcher focused on AI infrastructure, inference servers, and the trust boundaries around model execution. The research described here was conducted in controlled environments and coordinated through CERT/CC. Technical details are shared to help maintainers and operators reduce risk.