The Budget Arrives in the Request: Five SGLang Multimodal CVEs With No Fixed Version
NVD published five records against SGLang — the inference engine with 37,042 stars and 9,490 forks that serves a large share of self-hosted LLM endpoints — within one second of each other at 14:17:11Z on 12 October 2026. CVE-2026-109225, 109226, 109227, 109228 and 109229 all carry the identical CVSS 4.0 vector AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H for 8.7 high, all name VulnCheck as the assigning CNA, all affect through 0.5.21 — the current release, cut 2 October — and none names a fixed version. Every record is vulnStatus: Deferred, so no NVD analysis is coming.
They are availability bugs, and we are not going to inflate them. What makes the batch worth reading is that four of the five are the same error committed in four different places, and the fifth inverts the relationship between a server and its callers in a way that is easy to reproduce by accident in any multimodal serving stack.
The limit runs after the allocation
Each record names a resource limit that exists and is applied too late. We read every path at the v0.5.21 tag.
- CVE-2026-109225 (CWE-770) —
/v1/audio/transcriptions. Inentrypoints/http_server.py, the handler validatesresponse_formatagainst a three-item list, returns 400 if it fails, and then callsaudio_data = await file.read(). The entire multipart upload is accumulated in one Python object before anything asks whether it is audio, whether the model exists, or how large it is. There is noContent-Lengthcheck and no app-level body-size middleware. - CVE-2026-109226 (CWE-409) —
load_audioinutils/common.pydecodes throughtorchcodec’sAudioDecoder, falling back tosoundfile, with no duration or sample-count bound. A small FLAC file of silence expands into a PCM buffer of whatever size the header declares. Reachable throughaudio_urlandaudio_datainputs. - CVE-2026-109227 (CWE-770) —
preprocess_videoinmultimodal/processors/qwen_vl.pydecodes frames at full source resolution and then applies themax_pixelslimit. The limit is real; it governs what reaches the model, not what reaches memory. - CVE-2026-109229 (CWE-409) — one line in
NanoNemotronVLImageProcessor.__init__. We read it at the tag:
That is a process-global Pillow setting. Constructing one model’s image processor disables decompression-bomb protection for every image decode in the server, including the generic loader inImage.MAX_IMAGE_PIXELS = Noneutils/common.pythat other models use. A small, highly compressed image declaring enormous dimensions is then decoded in full.
The Nano-Nemotron line is the one to keep. It is a single assignment, written to make one model’s legitimately large images load, and its blast radius is the whole process. A contributor in the issue thread traces it to the original model port and states that nothing in the processor depends on it — which, if correct, makes this the cheapest fix in the batch and the clearest illustration of why per-model preprocessing code should not be allowed to mutate library-global state. Serving stacks that load several model families into one process inherit the most permissive setting any of them chose.
The caller sets the budget
CVE-2026-109228 (CWE-770) is structurally different and, we think, the most instructive record here. GLM-V video processing in SGLang has deployment-side limits: frames per second, maximum frames, tokens per frame, total image tokens. The advisory says a remote attacker can override them by supplying those values in the video_url object. We checked the schema in entrypoints/openai/protocol.py, and the request model accepts exactly that:
class ChatCompletionMessageContentVideoURL(BaseModel):
url: str
max_dynamic_patch: Optional[int] = None
min_dynamic_patch: Optional[int] = None
fps: Optional[float] = None
max_frames: Optional[int] = None
max_tokens_per_frame: Optional[int] = None
max_image_tokens: Optional[int] = None
And in multimodal/processors/glm4v.py, the merge is explicit about precedence:
def _merge_glm_video_configs(default_config, item_configs):
defaults = dict(default_config or {})
return [{**defaults, **dict(config or {})} for config in item_configs]
Per-item values — the ones that arrive in the request body — are spread after the deployment defaults, so they win. Downstream, _hf_sample_frame_indices returns early when max_frames is set by the caller, bypassing the model processor’s own frame sampling, and the resize step keys off the caller’s max_tokens_per_frame.
Read as an API design decision, this is defensible and common: these look like per-request quality knobs, the kind of thing a client should be able to tune when it wants a cheaper or a sharper read of a video. Read as a security boundary, it is a configuration file that the client is allowed to edit. The distinction that matters is not whether a parameter is caller-supplied but whether it can be set above the server’s value. A per-request max_frames that is clamped to min(request, deployment) is a feature; the same field spread over the default is a budget bypass. Four lines of Python separate the two, and the version that works is also the one a cost-conscious operator wants.
Twenty-two hours from public issue to CVE, and no fix in either
The disclosure shape is unusual enough to record. All five findings were filed as public GitHub issues by JPengLi on 11 October between 15:42:19Z and 15:52:43Z — #43705, #43707, #43708, #43709 and #43710 — each with the file and line, a reproduction, and in the transcription case a negative control. The VulnCheck advisories and the NVD records followed roughly twenty-two hours later. At the time of writing, all five issues are open, SGLang’s newest release remains v0.5.21, and no record names a fixed version. The repository is active — it was pushed again at 21:50Z on 12 October.
What filled the gap is worth noting for anyone who argues that public filing is reckless. Within fourteen minutes of #43707, a contributor had identified the one-line removal and offered a regression test. On #43709, another contributor confirmed the behaviour on main, articulated the design choice (per-endpoint cap versus app-wide body-size middleware, and whether to reuse the existing media_url_max_file_size_mb naming), and on 12 October at 20:31Z opened a pull request implementing a 25 MiB per-endpoint limit chosen to match OpenAI’s documented cap for the same endpoint. That is a functioning disclosure process operating entirely in public — and it is still, as of now, zero shipped artefacts. The distance between “a correct patch exists in a pull request” and “a version you can install” is the gap this site has documented in PraisonAI, nginx-ui and Emacs TRAMP within the last week. The failure mode is not neglect; it is release cadence.
Why availability bugs in an inference server are not a footnote
The honest framing: VC:N/VI:N/VA:H is correct. Nothing here reads data or runs code. But the deployment pattern around inference endpoints makes availability unusually load-bearing. An SGLang worker holds model weights in GPU and host memory; it is not a stateless process that a supervisor restarts for free. A single OOM kill drops every in-flight request on that worker, and the restart cost is measured in model load time, not milliseconds. Multimodal endpoints are also the ones most likely to be exposed to semi-trusted callers — an internal tool, a partner integration, an agent pipeline that forwards user-supplied media. And three of the five paths are triggered by a URL (audio_url, video_url), which means the expensive object need not even transit your network boundary as an upload.
This is also a different SGLang story from CVE-2026-93034, which we covered on 9 October: that one was a deserialisation path where the fix changed the envelope and not the decoder. The common thread across both is where this project’s defects cluster — not in scheduling or kernels, but in the layer that turns an untrusted request into a tensor.
What to do, given there is nothing to upgrade to
- Cap the body at the proxy. The transcription path is fixed for you by any reverse proxy with a request-size limit —
client_max_body_sizein nginx, an equivalent in Envoy or your ingress. This is the one mitigation that is entirely outside SGLang and should be in place regardless. - Strip caller-supplied budget fields. If you serve GLM-V, have the gateway delete
fps,max_frames,max_tokens_per_frame,max_image_tokens,max_dynamic_patchandmin_dynamic_patchfrom everyvideo_urlobject before the request reaches SGLang, or clamp them to your own values. Doing this at the gateway is strictly better than waiting for the server-side clamp, because it also covers whatever the next model processor adds to that schema. - Do not expose multimodal endpoints to untrusted callers without a resource broker. Per-caller concurrency limits matter more than per-request size limits here: every one of these records describes exhaustion through concurrent requests, not a single oversized one.
- Check whether a Nano-Nemotron processor is constructed anywhere in your process. If it is, Pillow’s decompression-bomb guard is off for every image your server decodes, not just that model’s. Pinning
Image.MAX_IMAGE_PIXELSback to a finite value after model load is a one-line operator-side mitigation while the removal is pending. - Fetch remote media yourself. If
audio_url/video_urlare enabled, the server dereferences attacker-chosen URLs. Fetch, size-check and transcode in a separate component and hand SGLang bytes you have already bounded.
Verification note. The five CVE identifiers, publication timestamps (14:17:11–14:17:12Z, 12 October 2026), CVSS 4.0 scores and vectors, CWE assignments, affected ranges, vulnStatus: Deferred, assigning CNA and reference lists were read first-hand from the NVD 2.0 API. The source excerpts from entrypoints/http_server.py, entrypoints/openai/protocol.py, multimodal/processors/glm4v.py, multimodal/processors/nano_nemotron_vl.py and utils/common.py were read at the v0.5.21 tag and are quoted verbatim. Issue numbers, authors, creation timestamps, open state, comment contents, the pull-request number and the repository’s star, fork, release and push data were read first-hand from the GitHub API. The contributor statements about the origin of the Pillow line and about the chosen 25 MiB cap are attributed to those contributors and not independently verified by us. We did not install SGLang, load any model, construct a decompression bomb or oversized upload, or send any request to any deployment; no proof of concept was executed. The analysis of where the limits sit relative to the allocations, the clamp-versus-override argument, and the operational reading of availability impact are our editorial analysis. We have no evidence of exploitation and imply none.
Sources:
- NVD — CVE-2026-109228: GLM-V resource budget bypass via request-supplied video parameters (8.7)
- NVD — CVE-2026-109229: process-wide disabling of Pillow decompression-bomb protection (8.7)
- NVD — CVE-2026-109225: unbounded multipart read on /v1/audio/transcriptions (8.7)
- sgl-project/sglang issue #43709 — transcription upload has no application-level size limit (open; reproduction and contributor patch discussion)
- sgl-project/sglang issue #43705 — request-controlled GLM-V video budgets bypass deployment limits (open)
- sgl-project/sglang
protocol.pyat v0.5.21 — the video_url schema accepting fps, max_frames and token budgets - VulnCheck advisory — SGLang through 0.5.21 resource budget bypass via GLM-V video parameters