vLLM through 0.29.0 fetches and fully materializes remote or inline media before enforcing its documented media controls (the VLLMMAXAUDIOCLIPFILESIZEMB compressed-audio size cap, default 25 MB, and the per-modality --limit-mm-per-prompt item limits). Across four ingress paths — the shared media-acquisition layer (HTTPConnection.getbytes()/asyncgetbytes()), the chat completions audiourl/base64 path, the batch speech runner, and the Rust frontend POST /tokenize route — the server reads the entire HTTP response body, base64-decodes the inline payload, or spawns one fetch/decode task per media part, and only then applies the limit (or, on some paths, never applies it). A remote attacker can therefore cause the API server or batch-runner process to allocate memory and consume outbound bandwidth proportional to an attacker-chosen body size or media item count before the request is rejected, resulting in pre-inference memory and bandwidth exhaustion (denial of service). The chat and batch surfaces require an API key when one is configured; the Rust frontend /tokenize route is unauthenticated by design. There is no code execution or data disclosure impact.
vLLM before 0.29.0 contains a resource-limit bypass vulnerability in PyNvVideoCodec decoder allocation where sampler subclass shadowing allows independent counter increments. Unauthenticated attackers can select different sampler subclasses in video requests to exceed configured decoder limits and exhaust unaccounted GPU memory.
vLLM versions before 0.29.0 contain a denial-of-service vulnerability in the cachesalt parameter accepted on OpenAI-compatible and Anthropic API endpoints, which lacks maximum length validation and is processed on the single EngineCore scheduler thread. Unauthenticated attackers can send HTTP requests with multi-hundred-megabyte salt values that trigger expensive pickle serialization and SHA-256 hashing, stalling the scheduler thread and denying service to all concurrent requests.
vLLM is an inference and serving engine for large language models. In versions from 0.22.1 through 0.28.0, the operator-supplied model revision pin (--revision / --code-revision) is not propagated to several Hugging Face artifact loads for the FunAudioChat and Tarsier2 architectures: the WhisperFeatureExtractor and speechtokenizer PreTrainedTokenizerFast loads in vllm/modelexecutor/models/funaudiochat.py and the Qwen2VLConfig.frompretrained call used by Tarsier2ProcessingInfo in vllm/modelexecutor/models/qwen2vl.py. As a result, deployments pinned to a reviewed revision still resolve these behavior-affecting processor, tokenizer, and config artifacts from the repository's default revision, so a later change to the upstream default branch can alter audio preprocessing, speech tokenizer behavior, or Tarsier2 configuration without any change to the operator's configured pin. This is a supply-chain integrity and reproducibility failure for pinned deployments; it is residual to the earlier fix tracked as GHSA-3ww4-5jv9-j5gm / CVE-2026-47155 and does not constitute remote code execution or a trustremotecode=False bypass. The issue is fixed in version 0.28.0.
vLLM before 0.29.0 fails to enforce decoder prompt-length validation on the disaggregated serving endpoint /inference/v1/generate. When the request contains a 'features' (multimodal) payload, vllm/entrypoints/serve/disagg/serving.py builds a multimodal EngineInput directly from the caller-supplied tokenids, and GenerateRequest.tokenids (vllm/entrypoints/serve/disagg/protocol.py) is not checked against modelconfig.maxmodellen. For multimodal processors that report skippromptlengthcheck=True (for example Nemotron Parse, Whisper, and FireRedLID), InputProcessor.validatepromptlen() returns immediately for both encoder and decoder prompts, so an overlong prompt becomes an EngineCoreRequest and reaches the worker input-batch copy into a fixed maxmodellen-wide NumPy row. A client able to reach the endpoint on an affected model configuration can therefore submit an overlong tokenids list to trigger a worker failure and denial of service. Fixed in 0.29.0.
vLLM versions 0.22.0 through 0.23.0 fail to validate stoptokenids against vocabulary bounds in Rust HTTP and gRPC frontends, allowing out-of-vocabulary token IDs to reach MinTokensLogitsProcessor. Attackers can submit requests with mintokens greater than zero and out-of-vocabulary stoptokenids to trigger CUDA tensor indexing failures that leave EngineCore in a fatal state requiring service restart.
Summary The audio decode-duration guard (maxdurations, env VLLMMAXAUDIODECODEDURATIONS, default 600s) that protects against audio decompression-bomb DoS is wired into only the speech-to-text path (/v1/audio/transcriptions). The chat audio path (/v1/chat/completions, inputaudio content parts) calls the same decoder with no limit, so an unauthenticated client can submit a few-KB compressed audio file that expands to multiple GB of float32 PCM at decode time, OOM-killing the worker. This is a distinct sibling of CVE-2026-5497 (video frame-count bomb, VideoMediaIO.loadbase64) and GHSA-pq5c-rjhq-qp7p (image) in the same media subsystem.
Verified against main at HEAD d78650c (2026-06-16); applicable to the latest release v0.23.0.
Details The guard rejects long audio during decode (before allocation), implemented in vllm/multimodal/media/audio.py: - loadaudiopyav — metadata reject (~82-98) and live sample-count reject (~129-136) - loadaudiosoundfile — frames reject (~165-174)
All are gated on if maxdurations is not None.
It is passed in exactly one place — the transcription serving layer: python .../speechtotext/base/serving.py:~170-174 loadaudio(buf, sr=..., maxdurations=self.maxaudiodecodedurations) self.maxaudiodecodedurations = envs.VLLMMAXAUDIODECODEDURATIONS (default 600)
The chat path never threads it: python vllm/multimodal/media/audio.py:237-238 def loadbytes(self, data: bytes) -> tuple[npt.NDArray, float]: return loadaudio(BytesIO(data), sr=None) # no maxdurations -> every guard above is skipped
Unauthenticated reachability chain (chat): parseinputaudio (chatutils.py) -> parseaudio -> connector.fetchaudio -> AudioMediaIO.loaddataurl -> loadbase64 -> loadbytes -> loadaudio(..., sr=None). The connector never passes maxdurations, and inline data: URLs need no HTTP fetch (so VLLMAUDIOFETCHTIMEOUT does not bound them). The OpenAI-compatible server has no auth by default (auth only when --api-key / VLLMAPIKEY is set).
Impact Unauthenticated remote denial of service (availability) via memory amplification on a default-no-auth endpoint, on any deployment serving an audio-capable model. Same class and impact as the sibling CVE-2026-5497 (video). CWE-770 / CWE-409.
Fix A fix was introduced in this MR: https://github.com/vllm-project/vllm/pull/45908
vLLM versions >=0.10.2 and <0.28.0 do not apply any audio decode-size or duration limit when extracting audio from video input for NanoNemotronVL models. In nanonemotronvl.py, extractaudiofromvideos calls loadaudiopyav(BytesIO(videobytes)) without the maxdurations or maxdecodebytes parameters, so neither VLLMMAXAUDIODECODEDURATIONS nor VLLMMAXAUDIODECODEBYTES is enforced (unlike the direct audio upload path in AudioMediaIO). When a NanoNemotronVL model is served with useaudioinvideo=True, an attacker who supplies a small, highly compressed video as multimodal input can force the server to allocate gigabytes of memory during audio decoding, resulting in a denial of service. Fixed in vLLM 0.28.0.
vLLM through 0.29.0 contains a memory corruption vulnerability in the Triton bincountkernel where prompt token IDs index the penalty prompt-presence bitset without bounds checking against vocabulary size. Attackers can submit multimodal audio requests with tokens equal to vocabulary size, causing out-of-bounds writes that corrupt concurrent requests' sampler state and alter repetition penalty behavior.
vLLM through 0.29.0 fails to properly clean up decode-side metadata for rejected inference requests in prefill/decode disaggregated deployments. Remote attackers can submit requests with maxtokens=0 to exhaust decode-worker memory without bound until the worker restarts.
vLLM versions before 0.28.0 fail to validate the lower bound of token IDs in the /v1/embeddings and /pooling endpoints, allowing unauthenticated attackers to crash the engine by submitting negative token IDs. A single request with a negative token ID triggers a CUDA device-side assertion that poisons the GPU context, causing all subsequent requests to fail until the process restarts.
vLLM through 0.29.0 contains a denial of service vulnerability in the NIXL connector's prefix caching implementation that fails to properly validate block counts across multi-prompt completion requests in prefill/decode disaggregated deployments. Attackers can trigger an assertion failure in NixlBaseConnectorWorker.applyprefixcaching by submitting completion requests with multiple prompts of varying lengths, causing the decode worker to terminate and become unavailable until restarted.
vLLM before 0.28.0 contains a remote code execution vulnerability in the LlavaOnevision2 processor loader that ignores the trustremotecode parameter when loading remote processor classes. Attackers can craft a malicious model with arbitrary code in processingllavaonevision2.py that executes with vLLM process authority even when trustremotecode is set to False.
vLLM versions before 0.28.0 fail to validate audio sample rate headers in the transcription endpoint, allowing authenticated clients to bypass duration checks. Attackers can submit forged FLAC headers with inflated sample rates to trigger excessive memory allocation and crash the API server process affecting all tenants.
Summary
A vulnerability in ASGI web servers and starlette's trust on those web servers enables an authentication bypass of the OpenAI API AuthenticationMiddleware, which was discovered during @x41sec's source code audit. It allows to use the API without providing the configured VLLMAPIKEY or --api-key.
Details
In https://github.com/vllm-project/vllm/blob/v0.14.0/vllm/entrypoints/openai/apiserver.py#L689-L692 the urlpath is taken from the URL, which is reconstructed by starlette based on the request scope.
py from starlette.datastructures import URL, Headers, MutableHeaders, State
...
urlpath = URL(scope=scope).path.removeprefix(rootpath) headers = Headers(scope=scope) if urlpath.startswith("/v1") and not self.verifytoken(headers): response = JSONResponse(content={"error": "Unauthorized"}, statuscode=401) return response(scope, receive, send) return self.app(scope, receive, send)
The request scope includes the request's Host: header and reconstructs the URL as shown below:
py f"{scheme}://{hostheader}{path}"
Neither starlette nor any of the ASGI servers (including uvicorn, which vllm uses) properly filter the Host: header for invalid characters. This allows an attacker to include special URL characters such as / or ? in the Host: header and thereby control the reconstructed URL and it's .path attribute.
FastAPI/starlette's routing uses the HTTP path and does not depend on the parsed url.path attribute, allowing attackers to reach an endpoint via a certain path while providing a different value in the .path.
Impact - Instances of vllm that use an API Key for the OpenAI API and expose the API to attackers. - Instances behind an RFC-conforming web server (such as nginx) are not affected.
Summary
The fix for GHSA-rwxx-mrjm-wc2m ("ReDoS via structuredoutputs.regex compiled without timeout") wrapped the regex compile in the xgrammar and outlines backends with compileregexwithtimeout (and, for outlines, validateregexisbuildable). The lm-format-enforcer backend was left unguarded: it compiles the attacker-supplied regex with no timeout and no buildability check. A single request with a catastrophic regex hangs the structured-output compile step and stalls the engine worker (denial of service).
Affected code (HEAD d6d39c1)
vllm/v1/structuredoutput/backendlmformatenforcer.py: - line 110: characterlevelparser = lmformatenforcer.RegexParser(grammarspec) — builds an interegular FSM from the attacker regex synchronously, no timeout. - line 155: validatestructuredoutputrequestlmformatenforcer returns immediately on if soparams.regex: — no validation.
Sibling backends that WERE patched by GHSA-rwxx: - backendxgrammar.py:92 → compileregexwithtimeout(...). - backendoutlines.py:65 → compileregexwithtimeout(...) (plus validateregexisbuildable).
lm-format-enforcer uses the same interegular DFA-construction primitive the advisory cites for the outlines backend.
Reproduction (runtime-verified against the sink)
The sink lmformatenforcer.RegexParser(<regex>) was exercised directly (this is exactly what the backend calls):
baseline '[0-9]{3}' -> 0.0002 s attacker '(a{1,300}){300}' -> DID NOT COMPLETE in 20 s (one core pegged at 100% in interegular FSM construction)
End-to-end: start vllm serve <model> --structured-outputs-config '{"backend":"lm-format-enforcer"}', then POST /v1/completions with {"structuredoutputs":{"regex":"(a{1,300}){300}"}, ...}. The request never returns; because grammar compile runs in the engine's structured-output path, concurrent requests stall = worker-level DoS. The identical request against the outlines backend is bounded by compileregexwithtimeout and returns a clean error.
Impact
Unauthenticated denial of service (vLLM ships with no authentication by default). One request pegs a CPU core and blocks the structured-output engine path.
Reachability precondition: the operator must have selected backend=lm-format-enforcer via --structured-outputs-config (the default is auto → xgrammar). This is the same opt-in tier as the outlines backend that GHSA-rwxx already covered.
Suggested remediation
Route the lm-format-enforcer regex compile (backendlmformatenforcer.py:110) through the same compileregexwithtimeout guard already applied to the xgrammar and outlines backends, and reject un-buildable / oversized patterns in validatestructuredoutputrequestlmformatenforcer.
vLLM up to and including 0.17.0 allows remote attackers to cause a Denial of Service via memory exhaustion. The AsyncMediaIO.fetchaudio and AsyncMediaIO.fetchimage functions in multimodal/inputs.py fetch user-supplied media URLs using aiohttp and call r.read() without enforcing a maximum response size, allowing an attacker to exhaust server memory by providing a URL to an arbitrarily large file.
Summary
An assert-based security check in vLLM's activation function loading allows any unauthenticated attacker to achieve arbitrary code execution on the server by publishing a malicious HuggingFace model, when vLLM runs in Python optimized mode (python -O or PYTHONOPTIMIZE=1).
Details
vLLM uses an assert statement at vllm/modelexecutor/layers/pooler/activations.py:48 as its sole security control to restrict which activation functions can be loaded from a HuggingFace model's config.json:
python vllm/modelexecutor/layers/pooler/activations.py:35-53 functionname: str | None = None if ( hasattr(config, "sentencetransformers") and "activationfn" in config.sentencetransformers ): functionname = config.sentencetransformers["activationfn"] elif ( hasattr(config, "sbertcedefaultactivationfunction") and config.sbertcedefaultactivationfunction is not None ): functionname = config.sbertcedefaultactivationfunction
if functionname is not None: assert functionname.startswith("torch.nn.modules."), ( "Loading of activation functions is restricted to " "torch.nn.modules for security reasons" ) fn = resolveobjbyqualname(functionname)()
Python's assert statements are stripped at compile time when running in optimized mode (python -O or PYTHONOPTIMIZE=1). When the assert is absent, the attacker-controlled functionname from the model's config.json is passed directly to resolveobjbyqualname() — an unrestricted import gadget:
python def resolveobjbyqualname(qualname: str) -> Any: modulename, objname = qualname.rsplit(".", 1) module = importlib.importmodule(modulename) return getattr(module, objname)
This is the same vulnerability class as CVE-2017-1000433 (pysaml2 assert-based auth bypass), flagged by Bandit B101 and Ruff S101, and the reason Django proactively replaced all assert-based security checks (ticket #32508).
Attacker-controlled input sources: - config.sentencetransformers["activationfn"] (line 40) - config.sbertcedefaultactivationfunction (line 45)
Affected call sites — getactfn() is called via resolveclassifieractfn() from: - vllm/modelexecutor/layers/pooler/seqwise/poolers.py:122 — SequencePooler - vllm/modelexecutor/layers/pooler/tokwise/poolers.py:130 — TokenPooler
Broader systemic risk: resolveobjbyqualname is called from ~20 locations across the codebase with no validation of its own. Any future caller feeding user-controlled input to it without validation creates the same vulnerability class.
Suggested fix: Replace the assert with an explicit conditional raise:
python if not functionname.startswith("torch.nn.modules."): raise ValueError( "Loading of activation functions is restricted to " "torch.nn.modules for security reasons" )
Impact
Arbitrary code execution. A malicious model author publishes a HuggingFace model with a crafted config.json. When a victim loads this model with vLLM running under python -O or PYTHONOPTIMIZE=1, arbitrary code executes during model initialization with the privileges of the vLLM process.
The attack requires: 1. Victim loads a malicious model from HuggingFace (user interaction) 2. vLLM runs under python -O or PYTHONOPTIMIZE=1 (documented in production use) 3. Model uses a cross-encoder architecture (e.g. BERT or RoBERTa with sequence classification)
Coordinated disclosure note: This vulnerability was also reported via huntr.com on April 2, 2026 (https://huntr.com/bounties/dcb05b04-e625-41e7-adbc-bbae0cc2d64c). A GitHub Security Advisory was also filed because it is vLLM's stated preferred disclosure channel per SECURITY.md.
Fix
A fix for this was introduced in this commit: https://github.com/vllm-project/vllm/commit/b3c7ffcab82c2439726f8cb213800f6f38c023d3
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."
Summary A Denial of Service vulnerability exists in the vLLM OpenAI-compatible API server. Due to the lack of an upper bound validation on the n parameter in the ChatCompletionRequest and CompletionRequest Pydantic models, an unauthenticated attacker can send a single HTTP request with an astronomically large n value. This completely blocks the Python asyncio event loop and causes immediate Out-Of-Memory crashes by allocating millions of request object copies in the heap before the request even reaches the scheduling queue.
Details The root cause of this vulnerability lies in the missing upper bound checks across the request parsing and asynchronous scheduling layers:
1. Protocol Layer: In vllm/entrypoints/openai/chatcompletion/protocol.py, the n parameter is defined simply as an integer without any pydantic.Field constraints for an upper bound. python class ChatCompletionRequest(OpenAIBaseModel): # Ordered by official OpenAI API documentation # https://platform.openai.com/docs/api/reference/chat/create messages: list[ChatCompletionMessageParam] model: str | None = None frequencypenalty: float | None = 0.0 logitbias: dict[str, float] | None = None logprobs: bool | None = False toplogprobs: int | None = 0 maxtokens: int | None = Field( default=None, deprecated="maxtokens is deprecated in favor of " "the maxcompletiontokens field", ) maxcompletiontokens: int | None = None n: int | None = 1 presencepenalty: float | None = 0.0
1. SamplingParams Layer (Incomplete Validation): When the API request is converted to internal SamplingParams in vllm/samplingparams.py, the verifyargs method only checks the lower bound (self.n < 1), entirely omitting an upper bounds check. python def verifyargs(self) -> None: if not isinstance(self.n, int): raise ValueError(f"n must be an int, but is of type {type(self.n)}") if self.n < 1: raise ValueError(f"n must be at least 1, got {self.n}.")
1. Engine Layer (The OOM Trigger): When the malicious request reaches the core engine (vllm/v1/engine/asyncllm.py), the engine attempts to fan out the request n times to generate identical independent sequences within a synchronous loop. python # Fan out child requests (for n>1). parentrequest = ParentRequest(request) for idx in range(parentparams.n): requestid, childparams = parentrequest.getchildinfo(idx) childrequest = request if idx == parentparams.n - 1 else copy(request) childrequest.requestid = requestid childrequest.samplingparams = childparams await self.addrequest( childrequest, prompttext, parentrequest, idx, queue ) return queue Because Python's asyncio runs on a single thread and event loop, this monolithic for-loop monopolizes the CPU thread. The server stops responding to all other connections (including liveness probes). Simultaneously, the memory allocator is overwhelmed by cloning millions of request object instances via copy(request), driving the host's Resident Set Size (RSS) up by gigabytes per second until the OS OOM-killer terminates the vLLM process.
Impact Vulnerability Type: Resource Exhaustion / Denial of Service
Impacted Parties: - Any individual or organization hosting a public-facing vLLM API server (vllm.entrypoints.openai.apiserver), which happens to be the primary entrypoint for OpenAI-compatible setups. - SaaS / AI-as-a-Service platforms acting as reverse proxies sitting in front of vLLM without strict HTTP body payload validation or rate limitations.
Because this vulnerability exploits the control plane rather than the data plane, an unauthenticated remote attacker can achieve a high success rate in taking down production inference hosts with a single HTTP request. This effectively circumvents any hardware-level capacity planning and conventional bandwidth stress limitations.
Summary
The VideoMediaIO.loadbase64() method at vllm/multimodal/media/video.py:51-62 splits video/jpeg data URLs by comma to extract individual JPEG frames, but does not enforce a frame count limit. The numframes parameter (default: 32), which is enforced by the loadbytes() code path at line 47-48, is completely bypassed in the video/jpeg base64 path. An attacker can send a single API request containing thousands of comma-separated base64-encoded JPEG frames, causing the server to decode all frames into memory and crash with OOM.
Details
Vulnerable code
python video.py:51-62 def loadbase64(self, mediatype: str, data: str) -> tuple[npt.NDArray, dict[str, Any]]: if mediatype.lower() == "video/jpeg": loadframe = partial(self.imageio.loadbase64, "image/jpeg") return np.stack( [np.asarray(loadframe(framedata)) for framedata in data.split(",")] # ^^^^^^^^^^ # Unbounded split — no frame count limit ), {} return self.loadbytes(base64.b64decode(data))
The loadbytes() path (line 47-48) properly delegates to a video loader that respects self.numframes (default 32). The loadbase64("video/jpeg", ...) path bypasses this limit entirely — data.split(",") produces an unbounded list and every frame is decoded into a numpy array.
video/jpeg is part of vLLM's public API
video/jpeg is a vLLM-specific MIME type, not IANA-registered. However it is part of the public API surface:
- encodevideourl() at vllm/multimodal/utils.py:96-108 generates data:video/jpeg;base64,... URLs - Official test suites at tests/entrypoints/openai/testvideo.py:62 and tests/entrypoints/testchatutils.py:153 both use this format
Memory amplification
Each JPEG frame decodes to a full numpy array. For 640x480 RGB images, each frame is ~921 KB decoded. 5000 frames = ~4.6 GB. np.stack() then creates an additional copy. The compressed JPEG payload is small (~100 KB for 5000 frames) but decompresses to gigabytes.
Data flow
POST /v1/chat/completions → chatutils.py:1434 videourl type → mmparser.parsevideo() → chatutils.py:872 parsevideo() → self.connector.fetchvideo() → connector.py:295 fetchvideo() → loadfromurl(url, self.videoio) → connector.py:91 loaddataurl(): urlspec.path.split(",", 1) → mediatype = "video/jpeg" → data = "<frame1>,<frame2>,...,<frame10000>" → connector.py:100 mediaio.loadbase64("video/jpeg", data) → video.py:54 data.split(",") ← UNBOUNDED → video.py:55-57 all frames decoded into numpy arrays → video.py:56 np.stack([...]) ← massive combined array → OOM
connector.py:91 uses split(",", 1) which splits on only the first comma. All remaining commas stay in data and are later split by video.py:54.
Comparison with existing protections
| Code Path | Frame Limit | File | |-----------|-------------|------| | loadbytes() (binary video) | Yes — numframes (default 32) | video.py:46-49 | | loadbase64("video/jpeg", ...) | No — unlimited data.split(",") | video.py:51-62 |
Summary
Two model implementation files hardcode trustremotecode=True when loading sub-components, bypassing the user's explicit --trust-remote-code=False security opt-out. This enables remote code execution via malicious model repositories even when the user has explicitly disabled remote code trust.
### Details
Affected files (latest main branch):
1. vllm/modelexecutor/models/nemotronvl.py:430 python visionmodel = AutoModel.fromconfig(config.visionconfig, trustremotecode=True)
2. vllm/modelexecutor/models/kimik25.py:177 python cachedgetimageprocessor(self.ctx.modelconfig.model, trustremotecode=True)
Both pass a hardcoded trustremotecode=True to HuggingFace API calls, overriding the user's global --trust-remote-code=False setting.
Relation to prior CVEs: - CVE-2025-66448 fixed automap resolution in vllm/transformersutils/config.py (config loading path) - CVE-2026-22807 fixed broader automap at startup - Both fixes are present in the current code. These hardcoded instances in model files survived both patches — different code paths.
Impact
Remote code execution. An attacker can craft a malicious model repository that executes arbitrary Python code when loaded by vLLM, even when the user has explicitly set --trust-remote-code=False. This undermines the security guarantee that trustremotecode=False is intended to provide.
Remediation: Replace hardcoded trustremotecode=True with self.config.modelconfig.trustremotecode in both files. Raise a clear error if the model component requires remote code but the user hasn't opted in.
Issue Description Librosa defaults to using numpy.mean for mono downmixing (tomono), while the international standard ITU-R BS.775-4 specifies a weighted downmixing algorithm. This discrepancy results in: - Inconsistency between audio heard by humans (e.g., through headphones/regular speakers) and audio processed by AI models (Which infra via Librosa, such as vllm, transformer).
https://github.com/librosa/librosa/blob/af8c839fb15317fa2712ea66e7a22da6a9267b32/librosa/core/audio.py#L478 Attack Scenario and Impact
LFE (Low-Frequency Effects) Channel Exploit Attackers can craft special multichannel audio files containing: 1. Normal content in front channels (L/R) 2. Either interference signals or hidden content in the LFE channel
Notice: It is worth noting that not only the LFE channel is excluded, but in fact, channels beyond the 6th (such as rear surround channels, overhead channels, height speakers, etc.) are also not supported.
Attack Methodology:
Attackers can create specially engineered multichannel audio with LFE interference, where front channels (L/R) contain normal content while the LFE channel carries interference signals or hidden content. When played on consumer devices that ignore LFE channels, only the normal content is heard. However, when processed by AI systems using Librosa (which mixes all channels), the LFE interference affects speech recognition feature extraction or masks critical detection features. This enables malicious content to bypass AI detection while still reaching end users, potentially compromising voice authentication systems, evading content moderation, or disrupting speech recognition accuracy.
Potential Exploitation Scenarios: - Voice authentication systems may be tricked into accepting anomalous audio - Content moderation systems may fail to detect prohibited content hidden in LFE channels - Speech recognition systems may produce incorrect transcriptions
Note: torch.audio implements this correctly. Failure to do so may lead to inconsistencies between training and test audio, resulting in performance degradation.
Resources
- ITU-R BS.775-4 Standard - Librosa Source Code - Librosa securty report
Fixes
- https://github.com/vllm-project/vllm/pull/37058, which removes the librosa dependency from vLLM.
vLLM versions 0.8.0 and later are vulnerable to an Out-of-Memory (OOM) Denial of Service (DoS) attack due to unbounded frame count processing in the VideoMediaIO.loadbase64() method. When processing video/jpeg data URLs, the method splits the base64 data string on commas to extract individual JPEG frames without enforcing a frame count limit. An attacker can exploit this by crafting a single API request containing thousands of comma-separated base64-encoded JPEG frames in a data URL, causing the server to decode all frames into memory and crash due to excessive memory consumption. This vulnerability is reachable via the OpenAI-compatible chat completions API and does not require authentication.
vLLM versions >= 0.10.2 and < 0.13.0 are missing sparse tensor validation in multimodal embeddings processing. Because PyTorch disables sparse tensor invariant checks by default, an attacker can submit crafted embedding requests with malformed (negative or out-of-bounds) tensor indices, when the prompt-embeds feature is enabled, to trigger crashes or resource exhaustion (denial of service), with potential for out-of-bounds/write-what-where memory corruption. This continues CVE-2025-62164, whose prior fix only disabled the feature by default rather than addressing the root cause.
Summary
The SSRF protection fix for https://github.com/vllm-project/vllm/security/advisories/GHSA-qh4c-xf7m-gxfc can be bypassed in the loadfromurlasync method due to inconsistent URL parsing behavior between the validation layer and the actual HTTP client.
Affected Component
- File: vllm/connections.py - Function: loadfromurlasync
Vulnerability Details
Root Cause
The SSRF fix uses urllib3.util.parseurl() to validate and extract the hostname from user-provided URLs. However, loadfromurlasync uses aiohttp for making the actual HTTP requests, and aiohttp internally uses the yarl library for URL parsing.
These two URL parsers handle backslash characters (\) differently:
| Parser | Input URL | Parsed Host | Parsed Path | Behavior | |--------|-----------|-------------|-------------|----------| | urllib3.parseurl() | https://httpbin.org\@evil.com/ | httpbin.org | /%5C@evil.com/ | URL-encodes \ as %5C, treats \@evil.com/ as part of the path | | yarl (via aiohttp) | https://httpbin.org\@evil.com/ | evil.com | / | Treats \ as part of userinfo (user: httpbin.org\), the @ acts as the userinfo/host separator |
Attack Scenario
python Attacker provides this URL maliciousurl = "https://httpbin.org\\@evil.com/"
1. Validation layer (urllib3.parseurl) parsed = urllib3.util.parseurl(maliciousurl) parsed.host == "httpbin.org" ✅ Passes validation
2. Actual request (aiohttp with yarl) async with aiohttp.ClientSession() as session: async with session.get(maliciousurl) as response: # Request actually goes to evil.com! ❌ Bypass!
Why This Happens
1. yarl: Interprets httpbin.org\ as the userinfo component, and @ as the userinfo/host separator, so the URL is parsed as user=httpbin.org\, host=evil.com, path=/ 2. urllib3: URL-encodes the backslash as %5C, so \@evil.com/ becomes /%5C@evil.com/ which is treated as part of the path, leaving host=httpbin.org
This inconsistency allows an attacker to: - Bypass the hostname allowlist check - Access arbitrary internal/external services - Perform full SSRF attacks
Fixes
- https://github.com/vllm-project/vllm/pull/34743
Summary A Server-Side Request Forgery (SSRF) vulnerability exists in the MediaConnector class within the vLLM project's multimodal feature set. The loadfromurl and loadfromurlasync methods obtain and process media from URLs provided by users, using different Python parsing libraries when restricting the target host. These two parsing libraries have different interpretations of backslashes, which allows the host name restriction to be bypassed. This allows an attacker to coerce the vLLM server into making arbitrary requests to internal network resources.
This vulnerability is particularly critical in containerized environments like llm-d, where a compromised vLLM pod could be used to scan the internal network, interact with other pods, and potentially cause Denial of Service or access sensitive data. For example, an attacker could make the vLLM pod send malicious requests to an internal llm-d management endpoint, leading to system instability by falsely reporting metrics like the KV cache state.
Details The core of the vulnerability lies in the MediaConnector.loadfromurl method and its asynchronous counterpart. These methods accept a URL string to fetch media content (images, audio, video).
def loadfromurl( self, url: str, mediaio: MediaIO[M], , fetchtimeout: int | None = None, ) -> M: # type: ignore[type-var] urlspec = urlparse(url) if urlspec.scheme.startswith("http"): self.asserturlinallowedmediadomains(urlspec) connection = self.connection data = connection.getbytes( url, timeout=fetchtimeout, allowredirects=envs.VLLMMEDIAURLALLOWREDIRECTS, ) return mediaio.loadbytes(data)
The URL validation uses the urlparse function from Python's urllib module, while the request is made using the request function from Python's requests module. The requests module's underlying URL parsing is implemented using the parseurl function from Python's urllib3. These two parsing functions follow different URL specifications; one is implemented according to the RFC 3986 specification, and the other is implemented according to the WHATWG Living Standard. There is a difference in how the two functions handle backslashes (\) in URLs, which allows the hostname restriction to be bypassed.
Fix
https://github.com/vllm-project/vllm/pull/32746
Summary
vLLM loads Hugging Face automap dynamic modules during model resolution without gating on trustremotecode, allowing attacker-controlled Python code in a model repo/path to execute at server startup.
---
Impact
An attacker who can influence the model repo/path (local directory or remote Hugging Face repo) can achieve arbitrary code execution on the vLLM host during model load. This happens before any request handling and does not require API access.
---
Affected Versions
All versions where vllm/modelexecutor/models/registry.py resolves automap entries with trygetclassfromdynamicmodule without checking trustremotecode (at least current main).
---
Details
During model resolution, vLLM unconditionally iterates automap entries from the model config and calls trygetclassfromdynamicmodule, which delegates to Transformers’ getclassfromdynamicmodule and executes the module code.
This occurs even when trustremotecode is false, allowing a malicious model repo to embed code in a referenced module and have it executed during initialization.
Relevant code
- vllm/modelexecutor/models/registry.py:856 — automap resolution - vllm/transformersutils/dynamicmodule.py:13 — delegates to getclassfromdynamicmodule, which executes code
---
Fixes
https://github.com/vllm-project/vllm/pull/32194
Credits
Reported by bugbunny.ai
Summary Short summary of the problem. Make the impact and severity as clear as possible. For example: An unsafe deserialization vulnerability allows any unauthenticated user to execute arbitrary code on the server.
Sending a pure prompt embeds payload in a /v1/completions request with a model using M-RoPE causes the EngineCore to fail an assertion and fatally crash, shutting down the entire server application.
Any remote user who is authorized to make a /v1/completions endpoint can trivially make such a request and induce a crash.
Details Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.
In commit 56669c1, a simple assert intended to be a type-narrowing assert was added to the initmropepositions method in GPUModelRunner (the offending line on main at the time of writing: https://github.com/vllm-project/vllm/blob/2d481f8a946ee0521872af0f098674a8ee01ce4a/vllm/v1/worker/gpumodelrunner.py#L1588-L1607).
python assert reqstate.prompttokenids is not None, ( "M-RoPE requires prompttokenids to be available." )
This type narrowing assert is to prevent mypy errors later in the function because None is not a valid type for mropemodel.getmropeinputpositions. Unfortunately, this assertion is not always true. /v1/completions requests that specify prompt=None and promptembeds=<not none> will indeed create a CachedRequestState where prompttokenids is None. This triggers the assertion, which in turn crashes the EngineCore and the Server application.
(EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/worker/gpumodelrunner.py", line 3997, in executemodel (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] deferredstatecorrectionsfn = self.updatestates(scheduleroutput) (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/worker/gpumodelrunner.py", line 1239, in updatestates (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] self.initmropepositions(reqstate) (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/worker/gpumodelrunner.py", line 1582, in initmropepositions (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] assert reqstate.prompttokenids is not None, ( (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (EngineCore pid=351) ERROR 06-11 00:48:03 [core.py:1167] AssertionError: M-RoPE requires prompttokenids to be available. (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] AsyncLLM outputhandler failed. (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] Traceback (most recent call last): (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/engine/asyncllm.py", line 660, in outputhandler (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] outputs = await enginecore.getoutputasync() (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] File "/usr/local/lib/python3.12/dist-packages/vllm/v1/engine/coreclient.py", line 1030, in getoutputasync (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] raise self.formatexception(outputs) from None (APIServer pid=1) ERROR 06-11 00:48:03 [asyncllm.py:704] vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause.
All requests using the /v1/chat/completions endpoints will have text/prompttokenids parts (corresponding to the chat template), and promptembeds parts are handled as mmfeatures. This method (rightly) filters out those promptembeds content parts as they are treated as text positions.
A sufficient solution to type narrowing here without raising a fatal assertion is to instead replace the assertion with a using dummy token ids:
python def initmropepositions(self, reqstate: CachedRequestState): model = self.getmodel() assert supportsmrope(model), "M-RoPE support is not implemented." mropemodel = cast(SupportsMRoPE, model)
# Filter out promptembeds modality (text-only position info) mropefeatures = [ f for f in reqstate.mmfeatures if f.modality != "promptembeds" ] # Handle both tokenids and embeddings-only inputs if reqstate.prompttokenids is not None: inputtokens = reqstate.prompttokenids elif reqstate.promptembeds is not None: # For text-only embeddings, dummy token IDs are safe since # getmropeinputpositions only uses len(inputtokens) when mmfeatures is empty seqlen = reqstate.promptembeds.shape[0] inputtokens = list(range(seqlen)) # Verify no mmfeatures remain (should be true after promptembeds filter) assert len(mropefeatures) == 0, ( "M-RoPE with promptembeds-only input should have no multimodal features" ) else: raise ValueError( "M-RoPE requires either prompttokenids or promptembeds." )
reqstate.mropepositions, reqstate.mropepositiondelta = ( mropemodel.getmropeinputpositions( inputtokens, mropefeatures, ) )
Technically, in isolation, this method still crashes in the case where reqstate.prompttokenids is None and reqstate.mmfeatures, so the solution above still leaves that potential vector open. As far as can be determined, however, such a reqstate is impossible in the first place in online mode, because it would require a /v1/completions request with promptembeds AND multimodal features, but the /v1/completions request schema does not expose multimodal inputs in any discernible way. Today, those are the only two endpoints with promptembeds support.
When in offline mode, it is technically possible to directly create an EngineCoreRequest that has promptembeds and not prompttokenids and mmfeatures, and pass that to LLM.generate. That would trigger this same assertion, and no validation would prevent that combination. It is strongly suspected, though, that this combination would be undefined in any model that support M-RoPE, because it would not be possible to determine which token positions correspond to mmfeatures. The proposed solution above would end up not setting reqstate.mropepositions and reqstate.mropepositiondelta in this scenario, which could result in undefined behavior.
promptembeds is far more familiar here than M-RoPE, and it is understood that each model that supports it is responsible for defining its own getmropeinputpositions which have varying implementations. There is insufficient knowledge to be prescriptive in how the two features should interact in the offline case, other than possibly raising a validation error earlier on preventing that combination (which would emulate the current assertion behavior). Regardless, in offline mode, the chances of a remote user being able to exploit this are slim-to-nil compared to the online case which is incredibly straightforward.
Impact What kind of vulnerability is it? Who is impacted?
- Denial of Service caused by an incorrect assertion inside of the GPUModelRunner which causes a fatal EngineCore exception - Any configuration with --enable-prompt-embeds and M-RoPE-supported model is vulnerable - The attack is extremely easy from the remote attacker's perspective (copying the official promptembeds online mode docs examples almost-verbatim, accounting for model-name and connection details, of course, will induce a guaranteed shutdown)
Summary
Current-head vLLM documents VLLMMAXAUDIOCLIPFILESIZEMB as the maximum audio file size accepted by the speech-to-text APIs. The default is 25 MB. vllm/envs.py also describes files larger than this value as rejected.
The /v1/audio/transcriptions and /v1/audio/translations routes call await request.file.read() before vLLM checks that limit. In FastAPI and Starlette, UploadFile.read() returns bytes from the uploaded file object; when called without a size argument, the route materializes the remaining file contents. vLLM then performs the compressed file-size check later in preprocessspeechtotext() against the already-created bytes object.
As a result, the documented compressed audio file-size limit does not bound the memory allocated by vLLM endpoint code before validation. An oversized multipart upload can cause vLLM to allocate memory proportional to the uploaded file size before rejecting the request as too large.
This is distinct from GHSA-6pr9-rp53-2pmc, which covered decoded PCM expansion after compressed input was accepted. This report covers compressed upload materialization before compressed-size validation.
Technical Details
The upload routes perform an unbounded read before vLLM checks the documented compressed audio file-size limit:
- vllm/entrypoints/speechtotext/transcription/apirouter.py: audiodata = await request.file.read() - vllm/entrypoints/speechtotext/translation/apirouter.py: audiodata = await request.file.read() - vllm/entrypoints/speechtotext/base/serving.py later checks: if len(audiodata) / 10242 > self.maxaudiofilesizemb
There is no route-level check of request.file.size, Content-Length, a bounded read(maxbytes + 1), or a streaming copy that stops at the configured limit before the full file is materialized.
This does not appear to be intended behavior. vLLM's security guide treats request-controlled resource use as a security boundary: for example, requests that exceed VLLMMAXNSEQUENCES are rejected before reaching the engine. The speech-to-text upload limit is documented in the same spirit as an API enforced limit, but the first vLLM check happens after the over-limit upload has already been copied into a bytes object.
Impact
Attack requirements:
- the deployment exposes /v1/audio/transcriptions or /v1/audio/translations; - a speech-to-text capable model/task is configured; and - the caller can submit requests to the endpoint, including any API key the deployment requires.
An API caller who meets those requirements can send an oversized audio file. vLLM reads the full uploaded file into memory before applying the configured compressed audio file-size limit. This can create memory pressure or, depending on process/container limits and concurrency, terminate the process before the request is rejected.
Suggested severity: Moderate, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H (6.5), CWE-770/CWE-400.
The impact is availability-only. This is not a claim of code execution, data access, cross-tenant data exposure, parser-level multipart memory exhaustion, or persistence after process restart. Deployment body-size limits at a reverse proxy or ASGI layer can mitigate the issue before vLLM sees the request, but vLLM's own documented file-size limit does not currently provide that memory boundary.
Suggested Fix
Enforce the compressed audio upload limit before the unbounded read:
- Check reliable upload size metadata before reading when available. - Read at most maxbytes + 1 bytes in chunks as a defense-in-depth guard against missing or unreliable metadata. - Share the helper between transcription and translation routes. - Add regression tests that prove an over-limit UploadFile is rejected without calling an unbounded read().
The important property is that over-limit compressed uploads are rejected before vLLM allocates the full uploaded file as bytes.
Resources
- vLLM security policy: https://github.com/vllm-project/vllm/security/policy - vLLM speech-to-text docs: https://docs.vllm.ai/en/latest/serving/onlineserving/speechtotext/ - vLLM security guide, request parameter resource limits: https://docs.vllm.ai/en/latest/usage/security/ - vLLM vulnerability management docs: https://docs.vllm.ai/en/latest/contributing/vulnerabilitymanagement/ - FastAPI file uploads: https://fastapi.tiangolo.com/reference/uploadfile/ - Starlette uploaded files: https://www.starlette.io/requests/ - Adjacent published audio advisory: https://github.com/vllm-project/vllm/security/advisories/GHSA-6pr9-rp53-2pmc - Request-parameter resource DoS precedent: https://github.com/vllm-project/vllm/security/advisories/GHSA-3mwp-wvh9-7528
Appendix: Affected Version
Validated against current head:
- commit: 1033ffac2eccf986fdd880f4dee64ca3b22c63c9 - described version: v0.22.1rc0-491-g1033ffac2e
Known affected range: current head. It has not been determined the introducing commit or release range.
Appendix: Proof Of Vulnerability
The attached proof is a non-destructive static probe. It does not upload a large file or contact a running vLLM server:
bash python3 attached-evidence/poc/audiouploadsizeprecheckprobe.py
Observed result:
json { "pov": "this report", "validated": true, "defaultlimitmb": 25, "documentedapilimit": true, "routes": { "transcriptionroute": { "unboundeduploadread": true, "earlysizeguardbeforeread": false, "chunkedboundedread": false }, "translationroute": { "unboundeduploadread": true, "earlysizeguardbeforeread": false, "chunkedboundedread": false } }, "latesizecheck": { "present": true } }
Expected behavior: vLLM rejects over-limit audio files before materializing the entire upload into memory in vLLM endpoint code.
Actual behavior: the route materializes the upload into memory first, and only then does vLLM reject the request as exceeding VLLMMAXAUDIOCLIPFILESIZEMB.