Where
-Infinity
0
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

vLLM from 0.14.0rc1 before 0.31.0 contains an information disclosure vulnerability in sanitizemessage() whose lowercase-only regex fails to scrub uppercase PIL image addresses. Unauthenticated attackers can send a crafted PNG to endpoints such as /tokenize, triggering HF-processor errors that leak live server heap addresses useful for ASLR bypass.

First published (updated )
Severity
6.9
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

vLLM from 0.24.0 before 0.31.0 contains a resource exhaustion vulnerability in the MiniMax M3 VL video processor, which accepts request-level minpixels without range or minpixels <= maxpixels validation. Its video-wide pixel budget is enforced only when maxlongsidepixel is set, and that value defaults to None. API clients can supply a large minpixels via mmprocessorkwargs to enlarge every decoded frame, causing excessive CPU and host-memory consumption that may disrupt workers or the service.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

vLLM from 0.19.0 before 0.31.0 contains a resource exhaustion vulnerability that allows inference clients to bypass the server frame limit by setting mediaiokwargs.video.numframes in chat completion requests. Attackers can supply numframes=-1 or a large value with many base64 JPEG frames, forcing VideoMediaIO to decode all frames and np.stack them, causing memory exhaustion.

First published (updated )
Severity
8.7
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

vLLM from 0.10.1 before 0.31.0 forwards a request-controlled mmprocessorkwargs.maxpatches value to the Hugging Face processor for Cohere2 Vision models without a server-side upper bound, causing quadratic CPU work during multimodal preprocessing. Remote clients can send small image chat-completion requests with large, varying maxpatches values to bypass the LRU cache, exhaust preprocessing workers, and deny service to other clients.

First published (updated )
Severity
8.2
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

vLLM from 0.28.0 before 0.30.0 contains a reachable assertion vulnerability in GPUModelRunner.initxdropepositions that allows remote attackers to crash the engine by submitting embeddings-only requests. Attackers can send a /v1/completions request containing only promptembeds to XD-RoPE models with --enable-prompt-embeds, terminating the EngineCore and API server for all clients.

First published (updated )
Severity
2.9
EPSS
0.07%
AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L/E:P/RL:U/RC:R

A vulnerability was found in vLLM up to 0.19.0. The affected element is the function hasmambalayers of the file vllm/v1/kvcacheinterface.py of the component KV Block Handler. Performing a manipulation results in uninitialized resource. It is possible to initiate the attack remotely. The attack is considered to have high complexity. The exploitability is described as difficult. The exploit has been made public and could be used. The existence of this vulnerability is still disputed at present. The proposed patch did not fix the issue. A 3rd party explains: "The divergence could be explained by a benign and expected vLLM behavior where vLLM server could group concurrent requests together resulting in different input shapes based on varying request arrival time. The differences in grouped input shapes could call different kernels with could produce difference results due to rounding and differences in order of operations. There is an environment variable VLLMBATCHINVARIANT=1 for users that desire to have deterministic output with temperature 0.0."

First published (updated )
Severity
7.5
Out-of-bounds Read
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary This report explains a Token Injection vulnerability in vLLM’s multimodal processing. Unauthenticated, text-only prompts that spell special tokens are interpreted as control. Image and video placeholder sequences supplied without matching data cause vLLM to index into empty grids during input-position computation, raising an unhandled IndexError and terminating the worker or degrading availability. Multimodal paths that rely on imagegridthw/videogridthw are affected. Severity: High (remote DoS). Reproduced on vLLM 0.10.0 with Qwen2.5-VL.

Details - Affected component: multimodal input position computation. - File/functions (paths are indicative): - vllm/modelexecutor/layers/rotaryembedding.py - getinputpositionstensor(...) - vlgetinputpositionstensor(...) - Failure mechanism: - The code counts detected vision tokens and then indexes videogridthw/imagegridthw accordingly. - When user input carries placeholder tokens but no actual multimodal payload, these grids are empty. The code does not bounds-check before indexing.

Representative snippet (context): python vllm/modelexecutor/layers/rotaryembedding.py @classmethod def vlgetinputpositionstensor( cls, inputtokens, hfconfig, imagegridthw, videogridthw, ..., ): # detect video tokens videonums = (visiontokens == videotokenid).sum() # later in processing t, h, w = ( videogridthw[videoindex][0], # IndexError if no video data videogridthw[videoindex][1], videogridthw[videoindex][2], )

Abbreviated call path: OpenAI API request → vllm.v1.engine.core: step/executemodel → vllm.v1.worker.gpumodelrunner: updatestates/executemodel → vllm.modelexecutor.layers.rotaryembedding: getinputpositionstensor → vlgetinputpositionstensor → IndexError: list index out of range

PoC Environment - vLLM: 0.10.0 - Model: Qwen/Qwen2.5-VL-3B-Instruct - Launch server: bash python -m vllm.entrypoints.openai.apiserver \ --model Qwen/Qwen2.5-VL-3B-Instruct \ --port 8000

Request (text-only, no image/video data) bash cat > request.json <<'JSON' { "model": "Qwen/Qwen2.5-VL-3B-Instruct", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "what's in picture <|visionstart|><|imagepad|><|visionend|>" } ] } ] } JSON

curl -s http://127.0.0.1:8000/v1/chat/completions \ -H 'Content-Type: application/json' \ --data @request.json

Observed result - HTTP 500; logs show IndexError: list index out of range from vlgetinputpositionstensor(...). - In some deployments, the worker exits and capacity remains reduced until manual restart.

Impact - Type: Token Injection leading to Remote Denial of Service (unauthenticated). A single request can trigger the fault. - Scope: Any vLLM deployment that serves VLMs and accepts raw user text via OpenAI-compatible endpoints (self-hosted or proxied/managed fronts). - Effect: Request → unhandled exception in position computation → worker termination / service unavailability.

Fixes

Changes associated with https://github.com/vllm-project/vllm/issues/32656

Credits Pengyu Ding (Infra Security, Ant Group) Ziteng Xu (Infra Security, Ant Group)

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Out-of-bounds Read
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

vLLM is an inference and serving engine for large language models (LLMs). From 0.6.1 to before 0.20.0, there is a a Token Injection vulnerability in vLLM’s multimodal processing. Unauthenticated, text-only prompts that spell special tokens are interpreted as control. Image and video placeholder sequences supplied without matching data cause vLLM to index into empty grids during input-position computation, raising an unhandled IndexError and terminating the worker or degrading availability. Multimodal paths that rely on imagegridthw/videogridthw are affected. This vulnerability is fixed in 0.20.0.

First published (updated )
Severity
2.9
AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P

A vulnerability was found in vLLM up to 0.19.0. The affected element is the function hasmambalayers of the file vllm/v1/kvcacheinterface.py of the component KV Block Handler. Performing a manipulation results in uninitialized resource. It is possible to initiate the attack remotely. The attack is considered to have high complexity. The exploitability is described as difficult. The exploit has been made public and could be used. The existence of this vulnerability is still disputed at present. The proposed patch did not fix the issue. A 3rd party explains: "The divergence could be explained by a benign and expected vLLM behavior where vLLM server could group concurrent requests together resulting in different input shapes based on varying request arrival time. The differences in grouped input shapes could call different kernels with could produce difference results due to rounding and differences in order of operations. There is an environment variable VLLMBATCHINVARIANT=1 for users that desire to have deterministic output with temperature 0.0."

First published (updated )
Severity
6.5
Incorrect Type Cast, Race Condition
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary

The extracthiddenstates speculative decoding proposer in vLLM returns a tensor with an incorrect shape after the first decode step, causing a RuntimeError that crashes the EngineCore process. The crash is triggered when any request in the batch uses sampling penalty parameters (repetitionpenalty, frequencypenalty, or presencepenalty).

A single request with a penalty parameter (e.g., "repetitionpenalty": 1.1) is sufficient to crash the server. The crash is deterministic and immediate — no concurrency, race condition, or special workload is required.

Details

In vLLM v0.17.0, the extracthiddenstates proposer's propose() method returned sampledtokenids.unsqueeze(-1), producing a tensor of shape (batchsize, 1).

In PR #37013 (first released in v0.18.0), the KV connector interface was refactored out of propose(). The return type changed from tuple[Tensor, KVConnectorOutput | None] to Tensor, and the .unsqueeze(-1) call was removed along with the KV connector output:

python Before (v0.17.0): return sampledtokenids.unsqueeze(-1), kvconnectoroutput # shape (batchsize, 1)

After (v0.18.0+): return sampledtokenids # shape (batchsize, 2) after first decode step

The refactor missed that sampledtokenids changed semantics between the first and subsequent decode steps. After the first decode step, the rejection sampler allocates its output as (batchsize, maxspeclen + 1). With numspeculativetokens=1, this produces shape (batchsize, 2) instead of the expected (batchsize, 1), causing a broadcast shape mismatch during penalty application.

Impact

Any vLLM deployment between v0.18.0 and v0.19.1 (inclusive) configured with extracthiddenstates speculative decoding is affected. A single API request containing any penalty parameter immediately and permanently crashes the EngineCore process, resulting in complete loss of service availability.

Patches

Fixed in PR #38610, first included in vLLM v0.20.0. The fix slices the return value to sampledtokenids[:, :1], ensuring the correct (batchsize, 1) shape regardless of the rejection sampler's output dimensions.

Workarounds

- Upgrade to vLLM v0.20.0 or later. - If upgrading is not possible, avoid using extracthiddenstates as the speculative decoding method on affected versions. - Alternatively, reject or strip penalty parameters (repetitionpenalty, frequencypenalty, presencepenalty) from incoming requests at an API gateway before they reach vLLM.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

The structuredoutputs.regex API parameter passes a user-supplied regex string directly to grammar compiler backends with no compilation timeout. In the xgrammar backend, the string reaches compileregex() with no guard. In the outlines backend, validateregexisbuildable() blocks structural issues (lookarounds, backreferences) but provides zero protection against exponential DFA state-space explosion. Patterns like (a+)+b pass all checks and hang the inference worker.

Root Cause

backendxgrammar.py:91 — no timeout: python ctx = self.compiler.compileregex(grammarspec)

backendoutlines.py:299–330 — structural checks only, no complexity analysis: python def validateregexisbuildable(regex: str) -> None: sreparse.parse(regex) # AST parse only — does not detect exponential patterns checkunsupported(...) # blocks lookarounds/backrefs, not nested quantifiers

backendoutlines.py:64 — no timeout: python oc.Index(regexstring, vocabulary.inner)

Impact

Denial of service — one request with an adversarial regex pattern hangs an inference worker indefinitely.

Remediation

Wrap compileregex() and oc.Index() calls in a thread with a deadline (e.g., 5 seconds). Add complexity analysis to validateregexisbuildable() to detect nested quantifier patterns before compilation.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

vLLM is a high-throughput and memory-efficient inference and serving engine for LLMs. Prior to 0.24.0, the structuredoutputs.regex API parameter passes a user-supplied regular expression string directly to the grammar compiler backends with no compilation timeout; in the xgrammar backend the string reaches the regex compiler with no guard, and in the outlines backend the validation step blocks structural issues such as lookarounds and backreferences but performs no complexity analysis, so a pattern with nested quantifiers passes all checks and causes exponential state-space expansion, allowing a single request containing an adversarial regex to hang an inference worker indefinitely and deny service. This issue is fixed in version 0.24.0.

First published (updated )
Severity
6.5
EPSS
0.05%
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Impact

A denial of service bug caused the vLLM server to crash if an invalid regex was provided while using structured output. This vulnerability is similar to GHSA-6qc9-v4r8-22xg, but for regex instead of a JSON schema.

Issue with more details: https://github.com/vllm-project/vllm/issues/17313

Patches

https://github.com/vllm-project/vllm/pull/17623

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

vLLM is an inference and serving engine for large language models (LLMs). Version 0.8.0 up to but excluding 0.9.0 have a Denial of Service (ReDoS) that causes the vLLM server to crash if an invalid regex was provided while using structured output. This vulnerability is similar to GHSA-6qc9-v4r8-22xg/CVE-2025-48942, but for regex instead of a JSON schema. Version 0.9.0 fixes the issue.

First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary

The Phi-4 multimodal audio encoder (phi4mmaudio.py / phi4mmutils.py) runs its conformer attention in eager mode by default: the official HF config of microsoft/Phi-4-multimodal-instruct does not set useptscaleddotproductattention, so the default False applies (phi4mmaudio.py:166, :876; phi4mmutils.py:1669). The eager path fully materializes the attention probability matrix of shape [nchunks, 16, 500, 500] per layer — and then stores it on the module: self.attn = attn (phi4mmutils.py:1802, a torch.jit.Attribute declared at :1693), keeping a live reference for every one of the 24 encoder layers.

The only gates on audio length are the server-side limits VLLMMAXAUDIOCLIPFILESIZEMB=25 and VLLMMAXAUDIODECODEDURATIONS=600 (envs.py:83-84) — i.e. a 10-minute clip is a legal input by default. A 600 s clip becomes ~60,000 mel frames, which the encoder unfolds into 500-frame chunks (phi4mmaudio.py:996, maxseqlen = 500), giving nchunks ≈ 120. Each layer therefore materializes a 120×16×500×500 matrix (~1–2 GiB depending on dtype), and the module attribute keeps all completed layers' matrices alive, so GPU memory climbs monotonically within a single request until an allocation fails.

Verified end-to-end on the official vllm/vllm-openai:v0.31.0 image: one 19.2 MB silent 600 s WAV → worker torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 2.29 GiB ... 501.75 MiB free ... 22.09 GiB allocated by PyTorch inside getaudiofeatures (phi4mmaudio.py:1279 → :1332 → phi4mmutils.py:1386) → EngineCore encountered a fatal error → API EngineDeadError → whole server down; /health refuses connections. Total time from request to dead service: ~6 seconds.

A secondary effect: because self.attn persists on the module after the request ends, the matrices of the last audio request stay resident indefinitely (observed +50 MiB pinned after a single 5 s control request); they are only overwritten by the next audio request, so the resident footprint equals the largest audio processed since startup.

Details

1. Root cause. MultiHeadedAttention.forwardattention in eager mode computes attn = maskedsoftmax(scores, mask) and unconditionally executes self.attn = attn (phi4mmutils.py:1795-1802). The attribute is a leftover inference/debug hook (used by torch.jit.Attribute(None, Tensor | None) at :1693); in serving it serves no purpose but pins the full [batch·chunks, heads, T, T] tensor per layer past both the layer's forward and the request's lifetime. The SDP attention branch (useptscaleddotproductattention=True) never materializes this matrix — but the HF config does not enable it and the code default is False.

2. Why one request suffices. The conformer chunks long audio to maxseqlen = 500 frames (phi4mmaudio.py:996). Duration is bounded only by VLLMMAXAUDIODECODEDURATIONS=600, so nchunks reaches ~120 for a maximal clip. With 16 heads and fp32 scores, one layer's attention tensor is ~1.8 GiB (the observed failing allocation was 2.29 GiB, consistent with the scores+bias materialization). The encoder has numblocks=24, attentionheads=16, attentiondim=1024, timereduction=8 (HF config). Weights plus a handful of retained per-layer matrices exhaust the ~2.3 GiB headroom that --gpu-memory-utilization 0.9 leaves on a 24 GB card — mid-forward, on the first request.

4. Deployment reality. --trust-remote-code is the documented, standard way to serve this model (the HF model card instructs it), so the finding applies to default deployments. The crash is deterministic, not load-dependent: scheduler stats at death show a single running request and near-zero KV cache usage.

Impact

Any unauthenticated client that can reach the chat/completions API of a default deployment serving Phi-4-multimodal-instruct (or any model reusing this audio tower) can kill the entire engine with one request carrying a perfectly legal 10-minute audio clip (silent noise suffices — no crafted content needed). Full denial of service, requiring a process restart. On GPUs with more headroom (e.g. 80 GB at default utilization) a single request may survive, but the eager path still materializes GiB-scale per-layer matrices and retains the last request's tensors indefinitely, so a few sequential max-length requests — or normal traffic following one — achieve the same result.

Relation to prior art: the only public Phi-4-mm advisory we found, GHSA-vc6m-hm49-g9qg, concerns a CPU-side quadratic-cost path — different subsystem, different mechanism. GitHub search for "phi4mm audio OOM/memory" (31 results, 2026-10-08) shows no report of this attention-retention issue; main (2026-10-08) is unchanged.

Suggested remediation (any one breaks the chain; 1+2 recommended):

1. Remove self.attn = attn from the serving path (or gate it behind an explicit debug/outputattentions flag) so attention matrices are freed when the layer returns. 2. Default useptscaleddotproductattention to True (flash/SDPA kernels never materialize the [B, H, T, T] matrix); keep eager only as an opt-in. 3. Additionally bound the encoder input (frames or duration) for this model so the eager path cannot reach GiB-scale single-layer allocations even if enabled.

This advisory was fixed in MR60614.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
EPSS
0.05%
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary Hitting the /v1/completions API with a invalid jsonschema as a Guided Param will kill the vllm server

Details The following API call (venv) [derekh@ip-172-31-15-108 ]$ curl -s http://localhost:8000/v1/completions -H "Content-Type: application/json" -d '{"model": "meta-llama/Llama-3.2-3B-Instruct","prompt": "Name two great reasons to visit Sligo ", "maxtokens": 10, "temperature": 0.5, "guidedjson":"{\"properties\":{\"reason\":{\"type\": \"stsring\"}}}"}' will provoke a Uncaught exceptions from xgrammer in ./lib64/python3.11/site-packages/xgrammar/compiler.py

Issue with more information: https://github.com/vllm-project/vllm/issues/17248

PoC Make a call to vllm with invalid jsonscema e.g. {\"properties\":{\"reason\":{\"type\": \"stsring\"}}}

curl -s http://localhost:8000/v1/completions -H "Content-Type: application/json" -d '{"model": "meta-llama/Llama-3.2-3B-Instruct","prompt": "Name two great reasons to visit Sligo ", "maxtokens": 10, "temperature": 0.5, "guidedjson":"{\"properties\":{\"reason\":{\"type\": \"stsring\"}}}"}' Impact vllm crashes

example traceback ERROR 03-26 17:25:01 [core.py:340] EngineCore hit an exception: Traceback (most recent call last): ERROR 03-26 17:25:01 [core.py:340] File "/home/derekh/workarea/vllm/vllm/v1/engine/core.py", line 333, in runenginecore ERROR 03-26 17:25:01 [core.py:340] enginecore.runbusyloop() ERROR 03-26 17:25:01 [core.py:340] File "/home/derekh/workarea/vllm/vllm/v1/engine/core.py", line 367, in runbusyloop ERROR 03-26 17:25:01 [core.py:340] outputs = stepfn() ERROR 03-26 17:25:01 [core.py:340] ^^^^^^^^^ ERROR 03-26 17:25:01 [core.py:340] File "/home/derekh/workarea/vllm/vllm/v1/engine/core.py", line 181, in step ERROR 03-26 17:25:01 [core.py:340] scheduleroutput = self.scheduler.schedule() ERROR 03-26 17:25:01 [core.py:340] ^^^^^^^^^^^^^^^^^^^^^^^^^ ERROR 03-26 17:25:01 [core.py:340] File "/home/derekh/workarea/vllm/vllm/v1/core/scheduler.py", line 257, in schedule ERROR 03-26 17:25:01 [core.py:340] if structuredoutputreq and structuredoutputreq.grammar: ERROR 03-26 17:25:01 [core.py:340] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ERROR 03-26 17:25:01 [core.py:340] File "/home/derekh/workarea/vllm/vllm/v1/structuredoutput/request.py", line 41, in grammar ERROR 03-26 17:25:01 [core.py:340] completed = self.checkgrammarcompletion() ERROR 03-26 17:25:01 [core.py:340] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ERROR 03-26 17:25:01 [core.py:340] File "/home/derekh/workarea/vllm/vllm/v1/structuredoutput/request.py", line 29, in checkgrammarcompletion ERROR 03-26 17:25:01 [core.py:340] self.grammar = self.grammar.result(timeout=0.0001) ERROR 03-26 17:25:01 [core.py:340] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ERROR 03-26 17:25:01 [core.py:340] File "/usr/lib64/python3.11/concurrent/futures/base.py", line 456, in result ERROR 03-26 17:25:01 [core.py:340] return self.getresult() ERROR 03-26 17:25:01 [core.py:340] ^^^^^^^^^^^^^^^^^^^ ERROR 03-26 17:25:01 [core.py:340] File "/usr/lib64/python3.11/concurrent/futures/base.py", line 401, in getresult ERROR 03-26 17:25:01 [core.py:340] raise self.exception ERROR 03-26 17:25:01 [core.py:340] File "/usr/lib64/python3.11/concurrent/futures/thread.py", line 58, in run ERROR 03-26 17:25:01 [core.py:340] result = self.fn(self.args, self.kwargs) ERROR 03-26 17:25:01 [core.py:340] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ERROR 03-26 17:25:01 [core.py:340] File "/home/derekh/workarea/vllm/vllm/v1/structuredoutput/init.py", line 120, in asynccreategrammar ERROR 03-26 17:25:01 [core.py:340] ctx = self.compiler.compilejsonschema(grammarspec, ERROR 03-26 17:25:01 [core.py:340] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ERROR 03-26 17:25:01 [core.py:340] File "/home/derekh/workarea/vllm/venv/lib64/python3.11/site-packages/xgrammar/compiler.py", line 101, in compilejsonschema ERROR 03-26 17:25:01 [core.py:340] self.handle.compilejsonschema( ERROR 03-26 17:25:01 [core.py:340] RuntimeError: [17:25:01] /project/cpp/jsonschemaconverter.cc:795: Check failed: (schema.is<picojson::object>()) is false: Schema should be an object or bool ERROR 03-26 17:25:01 [core.py:340] ERROR 03-26 17:25:01 [core.py:340] CRITICAL 03-26 17:25:01 [coreclient.py:269] Got fatal signal from worker processes, shutting down. See stack trace above for root cause issue.

Fix

https://github.com/vllm-project/vllm/pull/17623

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

vLLM is an inference and serving engine for large language models (LLMs). In versions 0.8.0 up to but excluding 0.9.0, hitting the /v1/completions API with a invalid jsonschema as a Guided Param kills the vllm server. This vulnerability is similar GHSA-9hcf-v7m4-6m2j/CVE-2025-48943, but for regex instead of a JSON schema. Version 0.9.0 fixes the issue.

First published (updated )
Severity
4.7
CSRF
AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N

Summary

The OpenAI-compatible server registers Starlette CORSMiddleware with allowedorigins defaulting to [""], and performs no authentication unless --api-key (or VLLMAPIKEY) is configured. A web page on any origin can send requests to a vLLM server that the victim's browser can reach (for example localhost or an internal/VPN address) and read the responses. The default allowcredentials=False blocks cookie-bearing cross-origin requests; because vLLM authenticates with a bearer header rather than cookies, the wildcard still permits untokenized access to an unauthenticated server and read access when the caller already holds a token.

Details

Affected components

- vllm/entrypoints/launchers/cliargs.py - allowedorigins default value. - vllm/entrypoints/serve/middleware/register.py - CORSMiddleware registration. - vllm/entrypoints/serve/middleware/authenticate.py - auth middleware and its guarded path prefixes.

Root cause

vllm/entrypoints/launchers/cliargs.py:

python allowcredentials: bool = False """Allow credentials.""" allowedorigins: list[str] = field(defaultfactory=lambda: [""]) """Allowed origins."""

vllm/entrypoints/serve/middleware/register.py:

python app.addmiddleware( CORSMiddleware, alloworigins=args.allowedorigins, allowcredentials=args.allowcredentials, allowmethods=args.allowedmethods, allowheaders=args.allowedheaders, )

The auth middleware is added only when a token is configured and guards only the /v1, /v2, /inference, and /cohere prefixes. /v1/loadloraadapter and /v1/unloadloraadapter are inside /v1 and therefore do require a key when one is set; they are open only when no key is configured and dynamic LoRA updating is enabled.

Impact

- A malicious page can enumerate models (GET /v1/models), issue POST /v1/chat/completions, consume GPU time, and read model output through the victim's browser. - When the server is reachable only from the victim's network, the browser acts as a proxy a remote website could not otherwise use. - With allowcredentials=False, cookie-based CSRF does not apply; the exposure is cross-origin request execution and response disclosure.

This advisory was fixed in MR61009.

1 / 2
Source: GitHub
First published (updated )
Severity
8.2
Input Validation
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

vLLM from 0.22.0 before 0.24.0 contains an improper input validation vulnerability in the Rust gRPC converter that allows remote attackers to bypass maxlogprobs limits via CandidateTokens.topn. Attackers can send Generate or GenerateStream requests with oversized or integer-wrapping topn values to trigger torch.topk failures that terminate EngineCore.

First published (updated )
Severity
8.2
Input Validation
AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

vLLM from 0.22.0 before 0.24.0 contains an improper input validation vulnerability in the Rust gRPC converter that allows remote attackers to bypass maxlogprobs limits via CandidateTokens.topn. Attackers can send Generate or GenerateStream requests with oversized or integer-wrapping topn values to trigger torch.topk failures that terminate EngineCore.

First published (updated )
Severity
8.2
Input Validation
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

vLLM from 0.22.0 before 0.24.0 contains an improper input validation vulnerability in the Rust gRPC Generate and GenerateStream service that forwards unchecked prompt token IDs to EngineCore. Attackers can submit requests with out-of-vocabulary prompt.tokenids values to trigger a fatal embedding lookup failure that terminates EngineCore and leaves the server unhealthy.

First published (updated )
Severity
8.2
Input Validation
AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

vLLM from 0.22.0 before 0.24.0 contains an improper input validation vulnerability in the Rust gRPC Generate and GenerateStream service that forwards unchecked prompt token IDs to EngineCore. Attackers can submit requests with out-of-vocabulary prompt.tokenids values to trigger a fatal embedding lookup failure that terminates EngineCore and leaves the server unhealthy.

First published (updated )
Severity
4.2
AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L

Affected

- Ecosystem / package: pip / vllm - Affected versions: vLLM ≤ 0.25.1 (confirmed on 0.25.1, commit 752a3a504485). The lower bound predates 0.25.1; maintainers can confirm how far back the flash late-interaction query cache reaches.

Summary

On late-interaction /score and /rerank deployments with flash late interaction enabled (the default for supported models), the worker caches per-request query embeddings under a key derived from the caller-controlled X-Request-Id header. A second concurrent request that reuses the victim's header value replaces the victim's cached query embedding before document scoring — so the victim's documents are scored against the attacker's query. Because the data-parallel router pins all requests sharing a cache key to the same engine, the collision is deterministic for an attacker who reuses the victim's X-Request-Id. Depending on timing, one request can also consume the shared use counter and force the other request into a late-interaction cache-miss error.

This is a remotely reachable, request-controlled cross-request integrity break on the standard scoring and reranking endpoints. It requires only that flash late interaction be enabled, which is the default for supported models.

Affected code

Links pinned to the confirmed commit 752a3a504485 (v0.25.1):

- vllm/entrypoints/serve/engine/serving.py#L117-L124 — baserequestid() copies the public X-Request-Id header directly. - vllm/entrypoints/pooling/base/serving.py#L109 — the frontend request id is f"{self.requestidprefix}-{self.baserequestid(rawrequest)}". - vllm/entrypoints/pooling/scoring/serving.py#L211 — flashlateinteraction() (at L191) derives worker cache keys directly from that id: querykeys = [f"{ctx.requestid}-query-{i}" for i in range(nqueries)]. - vllm/v1/pool/lateinteraction.py#L30-L36 — the data-parallel routing helper pins all requests sharing a querykey to the same engine via crc32(querykey), making collisions deterministic. - vllm/v1/worker/gpu/pool/lateinteractionrunner.py#L95 — the worker stores query embeddings in a process-local cache keyed only by that string: self.querycache[querykey] = output.clone().

The caller-controlled header enters as the request id, and the flash late-interaction path derives the worker cache key directly from it:

python vllm/entrypoints/serve/engine/serving.py Lines 116-126 @staticmethod def baserequestid( rawrequest: Request | None, default: str | None = None ) -> str | None: """Pulls the request id to use from a header, if provided""" if rawrequest is not None and ( (reqid := rawrequest.headers.get("X-Request-Id")) is not None ): return reqid

return randomuuid() if default is None else default

python vllm/entrypoints/pooling/scoring/serving.py Lines 207-212 nqueries = ctx.nqueries ndocs = len(ctx.engineinputs) - nqueries queryengineinputs = ctx.engineinputs[:nqueries]

querykeys = [f"{ctx.requestid}-query-{i}" for i in range(nqueries)] queryuses = [ndocs if nqueries == 1 else 1] nqueries

The worker then stores and reads the query embedding under that string with no check that the reader owns the entry — a colliding key returns another request's cached query, or (once the use counter is exhausted) raises a cache-miss error:

python vllm/v1/worker/gpu/pool/lateinteractionrunner.py Lines 91-107 if mode == LATEINTERACTIONMODECACHEQUERY: assert queryuses is not None # output can be a view into the current step's hidden-states # buffer, so clone it before storing across scheduling steps. self.querycache[querykey] = output.clone() self.queryuses[querykey] = queryuses outputs[i] = torch.zeros((), device=output.device, dtype=torch.float32) continue

if mode == LATEINTERACTIONMODESCOREDOC: queryoutput = self.querycache.get(querykey) if queryoutput is None: raise ValueError( "late-interaction query cache miss for key " f"{querykey!r}. Ensure query requests are executed " "before their paired document requests." )

The bug is specific to the flash late-interaction path. Non-flash late-interaction scoring computes MaxSim directly from one request's in-memory outputs and does not create a cross-request worker cache key.

Impact

A network client of the standard scoring API can, on a flash late-interaction /score or /rerank deployment:

1. Corrupt another user's results — by reusing the victim's X-Request-Id, the attacker's query embedding overwrites the victim's cached entry, so the victim's documents are scored against the attacker's query (a cross-request integrity break). 2. Induce errors — depending on timing, one request consumes the shared use counter and forces the other request into a late-interaction cache-miss error.

Both consequences follow deterministically from reusing the victim's header value, because same-key work is pinned to one engine. This is reachable through normal request handling and does not depend on any trusted inter-node network.

Suggested Fix

Derive the flash late-interaction query-cache key from a server-generated, unforgeable per-request identifier (a randomuuid() namespace) rather than the caller-supplied X-Request-Id, and thread that key through the PoolingServeContext to the doc-scoring pass so both passes reuse the same key and a caller cannot address another request's cache entry.

In vllm/entrypoints/pooling/scoring/serving.py, the encode-queries pass mints a fresh namespace and stashes the keys on the context:

python - querykeys = [f"{ctx.requestid}-query-{i}" for i in range(nqueries)] + querynamespace = randomuuid() + querykeys = [ + f"late-interaction-{querynamespace}-query-{i}" for i in range(nqueries) + ] + ctx.lateinteractionquerykeys = querykeys

and the encode-docs pass reads those stored keys instead of re-deriving them from ctx.requestid:

python - querykeys = [f"{ctx.requestid}-query-{i}" for i in range(nqueries)] + querykeys = ctx.lateinteractionquerykeys + if querykeys is None: + raise RuntimeError("Late-interaction query keys were not initialized.")

This requires adding the lateinteractionquerykeys: list[str] | None = None field to PoolingServeContext (vllm/entrypoints/pooling/typing.py). Because the namespace is a server-generated UUID, colliding X-Request-Id values no longer produce a shared cache key; a regression test asserting exactly that (colliding request ids yield distinct query-cache keys) accompanies the change.

Credit

Reported by: Patch the Planet (Trail of Bits + OpenAI collaboration)

This vulnerability was discovered using GPT-5.5-Cyber as part of the Patch the Planet security initiative.

---

Proposed fix: a fix for this issue is proposed in a public pull request: https://github.com/vllm-project/vllm/pull/51445

1 / 2
Source: GitHub
First published (updated )
Severity
4.2
AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L

vLLM is an inference and serving engine for large language models. Prior to 0.30.0, flash late-interaction scoring at the /score and /rerank endpoints derives each worker's querykey value from the caller-controlled X-Request-Id header. A concurrent request that reuses a victim's identifier can overwrite the cached query embedding so the victim's documents are scored against the attacker's query, and shared use counters can also cause a late-interaction cache-miss error. This issue is fixed in version 0.30.0.

First published (updated )
Severity
5.4
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:L

Summary

A Server Side Request Forgery (SSRF) vulnerability in downloadbytesfromurl allows any actor who can control batch input JSON to make the vLLM batch runner issue arbitrary HTTP/HTTPS requests from the server, without any URL validation or domain restrictions.

This can be used to target internal services (e.g. cloud metadata endpoints or internal HTTP APIs) reachable from the vLLM host.

------

Details

Vulnerable component

The vulnerable logic is in the batch runner entrypoint vllm/entrypoints/openai/runbatch.py, function downloadbytesfromurl:

runbatch.py Lines 442-482 async def downloadbytesfromurl(url: str) -> bytes: """ Download data from a URL or decode from a data URL.

Args: url: Either an HTTP/HTTPS URL or a data URL (data:...;base64,...)

Returns: Data as bytes """ parsed = urlparse(url)

# Handle data URLs (base64 encoded) if parsed.scheme == "data": # Format: data:...;base64,<base64data> if "," in url: header, data = url.split(",", 1) if "base64" in header: return base64.b64decode(data) else: raise ValueError(f"Unsupported data URL encoding: {header}") else: raise ValueError(f"Invalid data URL format: {url}")

# Handle HTTP/HTTPS URLs elif parsed.scheme in ("http", "https"): async with ( aiohttp.ClientSession() as session, session.get(url) as resp, ): if resp.status != 200: raise Exception( f"Failed to download data from URL: {url}. Status: {resp.status}" ) return await resp.read()

else: raise ValueError( f"Unsupported URL scheme: {parsed.scheme}. " "Supported schemes: http, https, data" )

Key properties:

- The function only parses the URL to dispatch on the scheme (data, http, https). - For http / https, it directly calls session.get(url) on the provided string. - There is no validation of: - hostname or IP address, - whether the target is internal or external, - port number, - path, query, or redirect target. - This is in contrast to the multimodal media path (MediaConnector), which implements an explicit domain allowlist. downloadbytesfromurl does not reuse that protection.

URL controllability

The url argument is fully controlled by batch input JSON via the fileurl field of BatchTranscriptionRequest / BatchTranslationRequest.

1. Batch request body type:

runbatch.py Line 67-80 class BatchTranscriptionRequest(TranscriptionRequest): """ Batch transcription request that uses fileurl instead of file.

This class extends TranscriptionRequest but replaces the file field with fileurl to support batch processing from audio files written in JSON format. """

fileurl: str = Field( ..., description=( "Either a URL of the audio or a data URL with base64 encoded audio data. " ), )

runbatch.py Line 98-111 class BatchTranslationRequest(TranslationRequest): """ Batch translation request that uses fileurl instead of file.

This class extends TranslationRequest but replaces the file field with fileurl to support batch processing from audio files written in JSON format. """

fileurl: str = Field( ..., description=( "Either a URL of the audio or a data URL with base64 encoded audio data. " ), )

There is no restriction on the domain, IP, or port of fileurl in these models.

1. Batch input is parsed directly from the batch file:

runbatch.py Line 139-179 class BatchRequestInput(OpenAIBaseModel): ... url: str body: BatchRequestInputBody @fieldvalidator("body", mode="plain") @classmethod def checktypeforurl(cls, value: Any, info: ValidationInfo): url: str = info.data["url"] ... if url == "/v1/audio/transcriptions": return BatchTranscriptionRequest.modelvalidate(value) if url == "/v1/audio/translations": return BatchTranslationRequest.modelvalidate(value)

runbatch.py Line 770-781 logger.info("Reading batch from %s...", args.inputfile)

# Submit all requests in the file to the engine "concurrently". responsefutures: list[Awaitable[BatchRequestOutput]] = [] for requestjson in (await readfile(args.inputfile)).strip().split("\n"): # Skip empty lines. requestjson = requestjson.strip() if not requestjson: continue

request = BatchRequestInput.modelvalidatejson(requestjson)

The batch runner reads each line of the input file (args.inputfile), parses it as JSON, and constructs a BatchTranscriptionRequest / BatchTranslationRequest. Whatever fileurl appears in that JSON line becomes batchrequestbody.fileurl.

1. fileurl is passed directly into downloadbytesfromurl:

runbatch.py Line 610-623 def wrapper(handlerfn: Callable): async def transcriptionwrapper( batchrequestbody: (BatchTranscriptionRequest | BatchTranslationRequest), ) -> ( TranscriptionResponse | TranscriptionResponseVerbose | TranslationResponse | TranslationResponseVerbose | ErrorResponse ): try: # Download data from URL audiodata = await downloadbytesfromurl(batchrequestbody.fileurl)

So the data flow is:

1. Attacker supplies JSON line in the batch input file with arbitrary body.fileurl. 2. BatchRequestInput / BatchTranscriptionRequest / BatchTranslationRequest parse that JSON and store fileurl verbatim. 3. maketranscriptionwrapper calls downloadbytesfromurl(batchrequestbody.fileurl). 4. downloadbytesfromurl’s HTTP/HTTPS branch issues aiohttp.ClientSession().get(url) to that attacker-controlled URL with no further validation.

This is a classic SSRF pattern: a server-side component makes arbitrary HTTP requests to a URL string taken from untrusted input.

Comparison with safer code

The project already contains a safer URL-handling path for multimodal media in vllm/multimodal/media/connector.py, which demonstrates the intent to mitigate SSRF via domain allowlists and URL normalization:

connector.py Lines 169-189 def loadfromurl( self, url: str, mediaio: MediaIO[M], , fetchtimeout: int | None = None, ) -> M: # type: ignore[type-var] urlspec = parseurl(url)

if urlspec.scheme and urlspec.scheme.startswith("http"): self.asserturlinallowedmediadomains(urlspec)

connection = self.connection data = connection.getbytes( urlspec.url, timeout=fetchtimeout, allowredirects=envs.VLLMMEDIAURLALLOWREDIRECTS, )

return mediaio.loadbytes(data)

and:

connector.py Lines 158-167 def asserturlinallowedmediadomains(self, urlspec: Url) -> None: if ( self.allowedmediadomains and urlspec.hostname not in self.allowedmediadomains ): raise ValueError( f"The URL must be from one of the allowed domains: " f"{self.allowedmediadomains}. Input URL domain: " f"{urlspec.hostname}" )

downloadbytesfromurl does not reuse this allowlist or any equivalent validation, even though it also fetches user-provided URLs.

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:L

vLLM is an inference and serving engine for large language models (LLMs). From 0.16.0 to before 0.19.0, a server-side request forgery (SSRF) vulnerability in downloadbytesfromurl allows any actor who can control batch input JSON to make the vLLM batch runner issue arbitrary HTTP/HTTPS requests from the server, without any URL validation or domain restrictions. This can be used to target internal services (e.g. cloud metadata endpoints or internal HTTP APIs) reachable from the vLLM host. This vulnerability is fixed in 0.19.0.

First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Summary

vLLM's /v1/responses endpoint accepts request-level chattemplatekwargs, but does not apply the --trust-request-chat-template gate that protects Chat Completions, render, tokenize, and pooling paths.

When the server is run with the documented default trustrequestchattemplate=False and no explicit --chat-template, a Responses request can pass:

json {"chattemplatekwargs": {"chattemplate": "<attacker Jinja template>"}}

That key is preserved by ChatParams.getapplychattemplatekwargs() and then bound as the chattemplate parameter to the shared HF renderer. Prior vLLM advisories establish that request-controlled Jinja chat templates can cause CPU/memory exhaustion. This report covers the current-head Responses sibling path missed by the earlier fixes.

Technical Details

The shared OpenAI frontend has a validation helper that rejects request-supplied chat templates unless the trust flag is enabled. It explicitly checks both top-level chattemplate and chattemplatekwargs["chattemplate"].

Responses is a separate serving path:

- vllm/entrypoints/openai/responses/protocol.py exposes chattemplatekwargs. - ResponsesRequest.buildchatparams() merges request kwargs. - OpenAIServingResponses.effectivechattemplatekwargs() uses those kwargs. - ChatParams.getapplychattemplatekwargs() preserves a request chattemplate key when the server chat template is None. - vllm/renderers/hf.py unpacks the result into safeapplychattemplate.

There is no equivalent trust gate in vllm/entrypoints/openai/responses/serving.py.

Impact

An authenticated caller who can submit /v1/responses requests can supply a request-controlled Jinja chat template despite the trust flag being disabled. The expected fixed behavior is the same as Chat Completions: reject chattemplatekwargs["chattemplate"] unless the server was started with --trust-request-chat-template.

Suggested Fix

- Add trustrequestchattemplate to OpenAIServingResponses. - Pass args.trustrequestchattemplate when constructing the Responses serving object. - Reject request.chattemplatekwargs["chattemplate"] before rendering when trust is false. - Keep ordinary kwargs such as enablethinking working. - Add regression tests for default-deny and opt-in-allow behavior on /v1/responses.

Affected Package/Versions

Source sweep found the affected shape in:

- v0.16.0 - v0.18.0 - v0.20.0 - v0.21.0 - v0.22.0 - v0.22.1 - v0.23.0 - current checkout 470229c37efaf69c86e8bc97482b0b1ff7551c65 - current origin/main b3f0a0a0df76dda92ec4b2c9335f77e84adad911

Suggested affected range:

text = 0.16.0, <= 0.23.0

Appendix: Duplicate Research

This is related to, but distinct from:

- GHSA-6fvq-23cw-5628 / CVE-2025-61620 - GHSA-69j4-grxj-j64p / CVE-2025-62426

Those advisories establish the security impact of request-controlled Jinja templates and chattemplatekwargs, but they cover older Chat Completions/tokenize implementations. This report covers a current /v1/responses sibling path that still lacks the trust gate.

Local prior submissions this report..029 do not cover this root cause.

This advisory was fixed in MR50300.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

vLLM after 0.24.0 and before 0.31.0 contains a memory exhaustion vulnerability in MOSS-Audio model serving that caches a processor for every distinct request-controlled mmprocessorkwargs value in an unbounded dictionary. Unauthenticated attackers can send distinct or oversized melconfig values to the /tokenize endpoint, which bypasses --api-key, to exhaust host memory and crash the API process.

First published (updated )
Severity
6.5
Input Validation, Incorrect Type Cast
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Affected

- Ecosystem / package: pip / vllm - Affected versions: vLLM ≤ 0.25.1 (confirmed on 0.25.1, commit 752a3a504485). The lower bound predates 0.25.1; maintainers can confirm how far back the scale-out transport path reaches.

Summary

vLLM's disaggregated scale-out transport splits a multimodal request into a trusted render step (POST /v1/chat/completions/render) and a separate generate step (POST /inference/v1/generate). The generate route decodes a caller-supplied features object — serialized encoder tensors (kwargsdata), multimodal hashes (mmhashes), placeholder ranges (mmplaceholders), and the internal field-processor selection — and forwards it into the engine as if it had come from the trusted renderer, with no rebinding to (or validation against) the active model's renderer contract. Because the two routes are ordinary auth-guarded HTTP endpoints (the /inference prefix is registered by default on generate-capable servers), any authenticated caller can submit an otherwise-valid render body with a single forged field.

Depending on which field is forged, this produces:

- an engine-fatal crash of the shared EngineCore process (denial of service), reproduced as a CUDA illegal-memory-access, a post-admission rank-mismatch ValueError, and a hard assert — three independent forged fields (sites 1, 2, 3); - silent cross-request encoder-cache poisoning / disclosure of shared encoder state when the cache-key hash is not bound to the payload (site 4); - transport-level integrity loss when sparse placeholder masks are dropped during render-to-generate replay (site 5).

All five share one root cause and one fix shape: the reconstructed multimodal state on the scale-out path is trusted without being rebound to, and validated against, the active model's renderer output before it reaches the engine.

These sites are distinct from prior multimodal hardening. Site 1 survives GHSA-wv77-2vpf-vmmg (that fix validates full tensor shape in MultiModalDataParser/getinputembeddings on the prompt-embeds path), because our request forges imagegridthw metadata with the pixel bytes intact and reaches the Qwen2 vision RoPE/cuseqlens and imageembeds.split sink, which the shape-check fix does not rebind. Site 4 is distinct from GHSA-c65p-x677-fgj6 (which folds metadata into MultiModalHasher.serializeitem to stop hash collisions), because the scale-out generate path trusts a caller-supplied mmhash as the cache key with no origin binding, so that fix does not stop a caller from submitting a victim's hash or a kwargsdata=None cache read.

Affected code

Links pinned to the confirmed commit 752a3a504485 (v0.25.1).

Shared entry point and control surface for all five sites:

- POST /inference/v1/generate route: vllm/entrypoints/scaleout/tokenintokenout/apirouter.py#L46-L75. - Public features schema (kwargsdata, mmhashes, mmplaceholders): vllm/entrypoints/scaleout/tokenintokenout/protocol.py#L42-L63. - ServingTokens.servetokens() copies the decoded geometry and hashes into engine structures without rebinding to the renderer schema: vllm/entrypoints/scaleout/tokenintokenout/serving.py#L145-L172. - The scale-out routers are registered by default on generate-capable servers and /inference is treated as an ordinary auth-guarded prefix: vllm/entrypoints/openai/apiserver.py#L217-L219.

Site 1 — forged Qwen grid geometry (engine-fatal DoS). The decoded imagegridthw is never rebound to the rendered pixel-tensor element count.

- Reconstruction with no model-specific geometry invariant: serving.py#L147-L172. - Qwen2VisionTransformer.prepareencodermetadata() derives RoPE tables, cuseqlens, and the FlashAttention maxseqlen from the caller-supplied grid: qwen2vl.py#L658-L712, consumed in forward() (#L713-L751). - processimageinput() computes imageembeds.split(sizes) from the same untrusted metadata: qwen2vl.py#L1348-L1369; M-RoPE positions at qwen2vl.py#L1223.

The only shape check is assert gridthw.ndim == 2; the split sizes and the vision-encoder call are then derived directly from the caller-supplied grid, with no cross-check against the pixel-tensor row count:

python vllm/modelexecutor/models/qwen2vl.py Lines 1348-1369 def processimageinput( self, imageinput: Qwen2VLImageInputs ) -> tuple[torch.Tensor, ...]: gridthw = imageinput["imagegridthw"] assert gridthw.ndim == 2

if imageinput["type"] == "imageembeds": imageembeds = imageinput["imageembeds"] else: pixelvalues = imageinput["pixelvalues"]

if self.usedataparallel: return rundpshardedmropevisionmodel( self.visual, pixelvalues, gridthw.tolist(), ropetype="rope3d" ) else: imageembeds = self.visual(pixelvalues, gridthw=gridthw)

# Split concatenated embeddings for each image item. mergesize = self.visual.spatialmergesize sizes = (gridthw.prod(-1) // mergesize // mergesize).tolist() return imageembeds.split(sizes)

Site 2 — wire-selected field-processor type confusion (engine-fatal DoS). MsgpackDecoder.decodemmfieldelem() trusts a wire-selected field-factory name and constructs the internal field processor directly from caller data.

- vllm/v1/serialutils.py#L440-L454 reads factorymethname, factorykw = obj["field"] and calls getattr(MultiModalFieldConfig, factorymethname). - Qwen2-VL field/schema contract: qwen2vl.py#L763-L791 and Qwen2VLImagePixelInputs (#L119-L144); parse/validate at qwen2vl.py#L1300. - Rank mismatch raised post-admission: vllm/utils/tensorschema.py#L155-L171, reached from TensorSchema.init → validate() (#L63).

Site 3 — non-positive placeholder length (engine-fatal DoS via reachable assert). PlaceholderRangeInfo{offset,length} is accepted as unconstrained integers and copied verbatim into the engine's PlaceholderRange.

- Schema: protocol.py#L28-L35. - Copied into PlaceholderRange: serving.py#L147-L153. - The only guard is an upper bound on embed count (numembeds > mmencodercachesize), with no non-positive check: vllm/v1/engine/inputprocessor.py#L459-L464; raw length returned by vllm/multimodal/inputs.py#L152-L154; window selection assumes non-empty ranges at vllm/multimodal/utils.py#L114-L134. - Sink: assert startidx < endidx at vllm/v1/worker/gpu/mm/encoderrunner.py#L114 (duplicated at vllm/v1/worker/gpumodelrunner.py#L3192); turned into a fatal shutdown by EngineCore's uncaught-exception path at vllm/v1/engine/core.py#L1229-L1233.

A length of 0 makes numencodertokens == 0, so endidx collapses to 0 and the bare assert fires inside the engine worker:

python vllm/v1/worker/gpu/mm/encoderrunner.py Lines 108-114 posinfo = mmfeature.mmposition startpos = posinfo.offset numencodertokens = posinfo.length

startidx = max(curquerystart - startpos, 0) endidx = min(curqueryend - startpos, numencodertokens) assert startidx < endidx

Site 4 — cache hash not bound to payload (integrity / disclosure). features.mmhashes (the cache key) and kwargsdata (the tensor) are independent fields with no origin or integrity binding.

- Schema exposing the caller-controlled hash and the None = resolve-from-cache semantics: protocol.py#L42-L63. - Handler forwards the caller's hashes unchanged into mminput(...): serving.py#L164-L170. - Input processing copies the caller hash into the feature identifier with only a string-type check: vllm/v1/engine/inputprocessor.py#L165-L181. - Sink — the receiver cache returns the cached tensor solely by that key (cachekey = feature.mmhash or feature.identifier): vllm/multimodal/cache.py#L602-L607.

The cache key is the caller-supplied hash with no verification against the tensor bytes, so a forged mmhash both stores under and reads back another request's slot:

python vllm/multimodal/cache.py Lines 601-607 for feature in mmfeatures: cachekey = feature.mmhash or feature.identifier self.touchreceivercacheitem(cachekey, feature.data)

for feature in mmfeatures: cachekey = feature.mmhash or feature.identifier feature.data = self.getandupdateitem(feature.data, cachekey) return mmfeatures

Site 5 — dropped sparse placeholder mask (transport integrity loss). The render path serializes placeholders as only offset/length, so models relying on sparse isembed masks lose the mask during render-to-generate replay.

- ServingRender.extractmmfeatures() builds each PlaceholderRangeInfo(offset=p.offset, length=p.length), discarding isembed: vllm/entrypoints/scaleout/render/serving.py#L212-L229. - Transport schema carries no field for the mask: protocol.py#L28. - ServingTokens.servetokens() reconstructs a dense PlaceholderRange regardless of the original: serving.py#L148-L152.

Impact

A single authenticated request to a scale-out multimodal deployment can:

- Crash the shared EngineCore process (sites 1, 2, 3), taking the served model down for every tenant (/health → 503). Availability-only; no code execution or data disclosure demonstrated for these sites. - Silently poison or read back another request's shared encoder-cache state (site 4) — an integrity/disclosure primitive. Attack complexity is High because the attacker must know or induce the victim's content hash; there is no availability impact for this site. - Corrupt backend-visible placeholder semantics across the render-to-generate boundary (site 5) for models that depend on sparse isembed masks.

The forged multimodal payload is small; only the trust in its self-declared geometry/identity is the defect.

Suggested Fix

On the scale-out path, do not trust caller-supplied multimodal state as renderer-produced. After decoding features, rebind and validate the reconstructed MultiModalKwargsItem against the active model's renderer contract at the HTTP boundary:

1. Reject any request whose decoded grid geometry is inconsistent with the rendered pixel-tensor element count and declared placeholder span, before it reaches prepareencodermetadata() (site 1). 2. Rebind each field's processor type to the schema the active model's renderer declares (or reject if it differs), instead of reconstructing internal field processors from wire-selected factory names (site 2). 3. Reject any PlaceholderRangeInfo with length <= 0 (or out-of-range offset) with a request-scoped 4xx, and convert the encoder-runner invariant into a checked, request-scoped error rather than a process-fatal assert (site 3). 4. Recompute or verify the content hash for submitted kwargsdata before using it as a cache key, and namespace receiver-cache keys to a server-generated or principal scope, refusing cache-reads for hashes the caller did not legitimately produce (site 4). 5. Serialize isembed in PlaceholderRangeInfo, validate its length against the placeholder span, and reconstruct it on replay (site 5).

Site 1 — validate grid geometry before the vision encoder. Replace the bare assert gridthw.ndim == 2 in processimageinput()/processvideoinput() with a shared helper that recomputes the split sizes and rejects a grid whose patch-row count does not match the pixel tensor (and rejects non-positive / non-merge-divisible dims), so the mismatch never reaches imageembeds.split():

python vllm/modelexecutor/models/qwen2vl.py — processimageinput() - gridthw = imageinput["imagegridthw"] - assert gridthw.ndim == 2 + gridthw = imageinput["imagegridthw"] + inputtype = imageinput["type"] + inputtensor = ( + imageinput["imageembeds"] + if inputtype == "imageembeds" + else imageinput["pixelvalues"] + ) + sizes = validateqwen2vlinputgeometry( + modality="image", + inputtype=inputtype, + inputtensor=inputtensor, + gridthw=gridthw, + spatialmergesize=self.visual.spatialmergesize, + ) ... - # Split concatenated embeddings for each image item. - mergesize = self.visual.spatialmergesize - sizes = (gridthw.prod(-1) // mergesize // mergesize).tolist() return imageembeds.split(sizes)

where the helper raises before the encoder runs:

python vllm/modelexecutor/models/qwen2vl.py — new validateqwen2vlinputgeometry() if t <= 0 or h <= 0 or w <= 0: raise ValueError(f"{modality} gridthw row {index} must be positive ...") if h % spatialmergesize != 0 or w % spatialmergesize != 0: raise ValueError(f"{modality} gridthw row {index} must be divisible ...") ... if actualrows != expectedrows: raise ValueError( f"{modality} {rowkind} do not match gridthw: " f"expected {expectedrows}, got {actualrows}." )

Site 3 — constrain the placeholder schema. Make PlaceholderRangeInfo reject non-positive lengths and negative offsets at the Pydantic boundary (plus parallel-length, non-overlapping, and within-prompt validators), turning the process-fatal assert into a request-scoped 422:

python vllm/entrypoints/scaleout/tokenintokenout/protocol.py — PlaceholderRangeInfo - offset: int - length: int + offset: int = Field(ge=0) + length: int = Field(gt=0)

Site 4 — bind the cache key to the payload. Derive each cache key by hashing the submitted serialized tensor (ignoring the caller's mmhashes) and refuse cache-only reads, so a forged hash can neither poison nor read a victim's slot:

python vllm/entrypoints/scaleout/tokenintokenout/serving.py — servetokens() + mmhashes = bindmmhashestokwargsdata( + features.mmhashes, features.kwargsdata, + ) engineinput = mminput( prompttokenids=request.tokenids, mmkwargs=MultiModalKwargsItems(mmkwargs), - mmhashes=features.mmhashes, + mmhashes=mmhashes, mmplaceholders=mmplaceholders, cachesalt=request.cachesalt, )

where bindmmhashestokwargsdata() raises on kwargsdata is None (cache-only read) and derives sha256(modality || "\0" || serializeditem) per item. Site 2 applies the same rebind-to-declared-schema pattern in mmserde.py (passing modality + mmprocessor into decodemmkwargsitem), and site 5 adds an isembed field to PlaceholderRangeInfo with fromplaceholderrange/toplaceholderrange helpers so the sparse mask survives render-to-generate replay. Each site fix ships with a regression test. This packet groups the five sites because they share one entry point (/inference/v1/generate + /render) and one root cause; we are happy to split it into per-component advisories (for example, engine-fatal input-validation vs. cache-key binding vs. transport-schema integrity) if the vLLM team prefers.

Credit

Reported by: Patch the Planet (Trail of Bits + OpenAI collaboration)

These vulnerabilities were discovered using GPT-5.5-Cyber as part of the Patch the Planet security initiative.

---

Proposed fix: a fix for this issue is proposed in a public pull request: https://github.com/vllm-project/vllm/pull/51898

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Input Validation, Incorrect Type Cast
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

vLLM is an inference and serving engine for large language models. Prior to 0.30.0, the /inference/v1/generate endpoint in the disaggregated scale-out path accepts caller-supplied tensors in the features.kwargsdata field, cache identifiers in the features.mmhashes field, ranges in the features.mmplaceholders field, and wire-selected multimodal field processors without rebinding them to the active model renderer contract. Forged grid geometry, field types, or non-positive placeholder lengths can terminate the shared EngineCore; when an attacker knows or can induce a victim's content hash, forged cache hashes can poison or retrieve cross-request encoder-cache state; and dropped sparse placeholder masks can alter replayed transport semantics. This issue is fixed in version 0.30.0.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203