GHSA-x6mc-67gf-chw4 CVE-2026-105758 PYSEC-2026-4310
vLLM: Qwen2-VL / Qwen3-VL video samplers bound on request-controlled max_frames, which the num_frames ceiling does not reach
- Severity
- 5.3 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.30%
- Chance of exploitation in the next 30 days. Higher than 23.10% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-770 Allocation of Resources Without Limits or Throttling
- Fix
0.30.0- Upgrade vllm to this version or later
Description
Summary
An unauthenticated remote attacker can exhaust the memory of the vLLM API-server process by raising the request-level media_io_kwargs.video.max_frames and fps knobs on any deployment serving a Qwen2-VL or Qwen3-VL model. 74 extra bytes of JSON took the server's peak RSS from 2 271 MiB to 13 629 MiB over unauthenticated POST /tokenize.
The num_frames ceiling reported in GHSA-vxqj-p4gw-9h4c and fixed by open PR #51969 does not reach this path: the Qwen samplers do not read num_frames at all. The same knobs were already capped upstream for GLMGAVideoBackend as an accepted security fix in 8b6de0eb9 (PR #54935, merged 2026-09-04); that cap never reached Qwen.
Details
Relationship to GHSA-vxqj-p4gw-9h4c and PR #51969 (read this first)
GHSA-vxqj-p4gw-9h4c reported that request-level media_io_kwargs.video.num_frames overrides the engine frame-count ceiling, and open PR #51969 fixes it by clamping num_frames inside VideoMediaIO.merge_kwargs.
That clamp does not reach the Qwen samplers. Qwen2VLVideoBackend and Qwen3VLVideoBackend do not read num_frames at all — Qwen2VLVideoBackend's own docstring says so ("num_frames is ignored (fps-driven, like the Qwen3-VL loader)"). They bound on max_frames, read from the same merged dict with no ceiling:
# vllm/multimodal/video.py — Qwen3VLVideoBackend.compute_frames_index_to_sample
min_frames = kwargs.get("min_frames", 4)
max_frames = kwargs.get("max_frames", 768)
num_frames = int(total_frames_num / original_fps * fps)
num_frames = min(max(num_frames, min_frames), max_frames, total_frames_num)
With max_frames raised from the request, the only remaining bound is total_frames_num — every frame in the container.
I applied PR #51969's patch locally and re-ran both paths through the real merge layer (merge_media_io_kwargs → VideoMediaIO.merge_kwargs → MediaConnector), against main @ b23433088:
request media_io_kwargs.video |
merged kwargs after #51969 | frames decoded | peak RSS |
|---|---|---|---|
| (absent) | None |
32 | 706 MiB |
{"video_backend":"opencv","num_frames":-1} |
{…,"num_frames":32} |
32 — fixed | 706 MiB |
{"video_backend":"qwen3_vl"} |
{…,"num_frames":32} |
60 | 779 MiB |
{"video_backend":"qwen3_vl","max_frames":1e9,"fps":1e6} |
{…,"max_frames":1000000000,"fps":1000000,"num_frames":32} |
900 — survives | 2 994 MiB |
#51969 does exactly what it claims for num_frames; the clamp writes num_frames: 32 into the merged dict and the Qwen sampler ignores it, while max_frames and fps pass through untouched.
The codebase already has the fix pattern, on other backends
This is not a new control being proposed. Commit 8b6de0eb9 — "[Security] Cap GLMGA video sampling to prevent request-driven resource exhaustion" (PR #54935, merged 2026-09-04, same author as #51969) — caps precisely these two knobs for GLMGAVideoBackend:
_MAX_FRAMES: ClassVar[int] = 640
_MAX_FPS: ClassVar[int] = 30
...
target_fps = min(target.fps, cls._MAX_FPS)
max_frames = min(kwargs.get("max_frames", cls._MAX_FRAMES), cls._MAX_FRAMES)
Its description states the root cause as "both target_fps and max_frames are controllable via request-level media_io_kwargs", and says it follows "the pattern established by GLM46VVideoBackend" (which caps via _MAX_FRAME_COUNT_DYNAMIC = 640 and _MAX_DURATION = 2400).
So two backends cap request-controlled fps/max_frames as an accepted security measure. Qwen2VLVideoBackend and Qwen3VLVideoBackend — the most widely deployed video models on vLLM — cap neither. In GLMGA the uncapped knobs sized an intermediate index list; in the Qwen samplers they size the decoded frame buffer, which is larger by the per-frame pixel count.
Reachability — default configuration, no authentication
media_io_kwargsis a request body field onChatCompletionRequestand is carried by/v1/chat/completions,/v1/embeddings,/v1/responses,/tokenizeand/invocations. No flag gates it.api_keydefaults toNone(vllm/entrypoints/launchers/cli_args.py), andAuthenticationMiddlewareis installed only when a key is configured — a defaultvllm serveis entirely unauthenticated.- Even with
--api-keyset,GUARDED_PREFIX = ("/v1", "/v2", "/inference", "/cohere")(vllm/entrypoints/serve/middleware/authenticate.py:11), so/invocations— which validates the sameChatCompletionRequestbody — and/tokenize— which performs full media ingestion — remain unauthenticated. MediaConnector.fetch_videoapplies the model's registered sampler only when the request did not name one (if "video_backend" not in video_io_kwargs), sovideo_backend: "qwen3_vl"is selectable on any deployment; request-level selection of stock sampler subclasses is already established as reachable by GHSA-j682-9xp5-rrf3. On a Qwen deployment novideo_backendkey is needed at all.- Default-on for any video-capable Qwen model.
The Rust frontend is not affected. rust/src/server/src/routes/openai/chat_completions/validate.rs:90 rejects media_io_kwargs with "media_io_kwargs is not supported." No second front is needed.
Impact
Unauthenticated remote denial of service by memory exhaustion of the API-server process. The decode runs in the frontend during chat parsing, before scheduling or admission control, so every tenant on the instance is affected. --limit-mm-per-prompt does not apply — it bounds media items, not frames within an item. CWE-770 / CWE-400.
Measured over unauthenticated HTTP: 1.43 MiB request body, 74 extra bytes of JSON, server peak RSS 2 271 → 13 629 MiB. In-process, the same request takes the decode from 60 to 900 frames. The frame count is bounded only by total_frames_num, which is the attacker's choice of video, and decoded bytes are frames × H × W × 3.
Context, measured on the num_frames path (GHSA-vxqj-p4gw-9h4c's path), not this one — these figures show what an unbounded frame count costs once the source video is chosen for it, and they transfer to this path because both converge on the same _read_frames_no_recovery allocation:
- 5.74 MiB request body → 9.27 GiB decoded, 9.73 GiB of new resident memory (1 736×).
- 117.6 MiB
video/jpegpayload → process OOM-killed:Out of memory: Killed process 182667 (python) total-vm:38665704kB, anon-rss:21121616kB
The two were not re-run at the larger sizes through the Qwen sampler; the 900-frame figure above is what I measured on this path.
Suggested fix
Extend the ceiling to the sampler-side knobs rather than clamping the single num_frames key. Two options, either acceptable:
- Per-backend caps, matching
8b6de0eb9. GiveQwen2VLVideoBackendandQwen3VLVideoBackendthe_MAX_FRAMES/_MAX_FPStreatment already applied toGLMGAVideoBackend, sokwargs.get("max_frames", …)andtarget.fpsare clamped to class constants. Smallest change; consistent with the accepted precedent. It leavesGlm5NextVideoBackend,Molmo2VideoBackend,NemotronVLVideoBackend,DynamicVideoBackendandOpenCVDynamicOpenPanguVideoBackendto be audited one by one, which is the current trajectory (#55727, #56207, #56390). - Strip the frame-count knobs at the merge boundary. In
VideoMediaIO.merge_kwargs, dropmax_frames/min_frames/fpsfromruntime_kwargsthe wayhw_decoders,pool_size,deviceand unconfigured GPU backends are already dropped there. That treats the whole frame-count family as startup-only in one place and is robust to future sampler subclasses, at the cost of removing a request-level knob some users may rely on. A clamp-don't-strip variant (request may lower, never raise) preserves the feature.
Option 2 composes with #51969 and needs no per-backend audit; I would favour it, but option 1 is the more conservative change and matches what has already been merged.
Affected versions
>= 0.24.0, through v0.29.1rc0 and main @ b23433088.
Lower bound established by probing release tags through the GitHub contents API; no version below is inferred, each was read out of the file at that tag.
Confirmed present — Qwen samplers reading unclamped max_frames (max_frames = kwargs.get("max_frames", 768) inside Qwen2VLVideoBackend / Qwen3VLVideoBackend in vllm/multimodal/video.py):
| ref | Qwen3VLVideoBackend present |
unclamped max_frames |
|---|---|---|
| v0.23.0 | no (class does not exist) | n/a |
| v0.24.0 | yes | yes |
| v0.25.0, v0.26.0, v0.27.0, v0.28.0, v0.29.0, v0.29.1rc0 | yes | yes |
main @ b23433088 |
yes | yes |
Confirmed present — request-level media_io_kwargs (field on the chat request model, and VideoMediaIO.merge_kwargs present): every ref probed, v0.19.0 through v0.29.1rc0 and main @ b23433088.
Confirmed absent — any num_frames ceiling clamp (PR #51969 unmerged): every ref probed, v0.19.0 through v0.29.1rc0 and main @ b23433088.
Not resolved, and why:
- The introducing commit/PR for
Qwen2VLVideoBackend/Qwen3VLVideoBackend. My clone is shallow (--depth=300), sogit log -S'max_frames'cannot reach it; the v0.23.0 → v0.24.0 boundary above is the tightest bound I established by probing release tags. - The rc tags between v0.23.0 and v0.24.0, to tighten the lower bound to a specific release candidate.
- Whether a
max_framesknob on a differently named pre-v0.24.0 backend is separately affected — at v0.23.0GLMGAVideoBackendalready carriedmax_frames = kwargs.get("max_frames", 640)with no clamp, and that clamp was only added on 2026-09-04 by8b6de0eb9. Versions between are plausibly affected through GLMGA rather than Qwen; I did not test that path. - Whether GHSA-vxqj-p4gw-9h4c's own affected range differs, which I cannot see — the advisory returns 404 to me.
No dependency versions are asserted anywhere in this report.
Weaknesses of this report, stated upfront
- No GPU was used. This host has no CUDA device, so the HTTP results come from the in-tree GPU-less render server rather than a full
vllm serve. That server runs the real frontend — the same request model, the samemerge_media_io_kwargs→VideoMediaIO.merge_kwargs→MediaConnector.fetch_videochain, the same/tokenizeroute — and media decoding happens entirely in the frontend, so I do not believe the engine's presence changes the result. I have not confirmed that on a GPU deployment, and a reviewer may reasonably want that repeated undervllm serve. - The in-process measurements (the #51969 comparison table) were taken by importing the tree directly at
b23433088, verified by__file__, with novllmwheel installed. - Amplification figures are a floor. The OpenCV build available here offers only
mp4v/XVID, giving ~2 200× compression on static content. An attacker using x264/x265 would do materially better for the same frame count. - The two large figures in the Impact section (9.73 GiB RSS; the OOM kill) were measured on the
num_framespath, not this one, and are labelled as such. - Per-frame pixels remain bounded by
VLLM_MAX_IMAGE_PIXELS; this concerns the unbounded frame count, which multiplies it. VLLM_MAX_MEDIA_DOWNLOAD_SIZE_MB(default 256) caps the compressed size of an HTTP-fetched video but not adata:URI, whichMediaConnector.load_from_urldispatches before any size logic is reached.
Prepared with AI assistance (Claude), per the repository's contributing guidance on disclosing AI-assisted contributions. All findings were verified by executing vLLM's own code at the commits cited; the analysis and the claims are my own.
Reported by Eva Crystal / 0xiviel (XSource Security).
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| vllm | >= 0.24.0, < 0.30.0 |
0.30.0 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
vllm PyPI → 0.30.0
- pip
pip install --upgrade "vllm>=0.30.0"- poetry
poetry add "vllm>=0.30.0"- uv
uv add "vllm>=0.30.0"
Severity breakdown
CVSS 3.1 5.3
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- None
- Integrity
- None
- Availability
- Low
Weakness
CWE-770: Allocation of Resources Without Limits or Throttling
The product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated.
How to prevent it
- Requirements: Clearly specify the minimum and maximum expectations for capabilities, and dictate which behaviors are acceptable when resource allocation reaches limits.
- Architecture and Design: Limit the amount of resources that are accessible to unprivileged users. Set per-user limits for resources. Allow the system administrator to define these limits. Be careful to avoid CWE-410.
- Architecture and Design: Design throttling mechanisms into the system architecture. The best protection is to limit the amount of resources that an unauthorized user can cause to be expended. A strong authentication and access control model will help prevent such attacks from occurring in the first place, and it will help the administrator to identify who is committing the abuse. The login application should be protected against DoS attacks as much as possible. Limiting the database access, perhaps by caching result sets, can help minimize the resources expended. To further limit the potential for a DoS attack, consider tracking the rate of requests received from users and blocking requests that exceed a defined rate threshold.
- Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright.
- Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
- Architecture and Design: Mitigation of resource exhaustion attacks requires that the target system either: recognizes the attack and denies that user further access for a given amount of time, typically by using increasing time delays uniformly throttles all requests in order to make it more difficult to consume resources more quickly than they can again be freed. The first of these solutions is an issue in itself though, since it may allow attackers to prevent the use of the system by a particular valid user. If the attacker impersonates the valid user, they may be able to prevent the user from accessing the server in question. The second solution can be difficult to effectively institute -- and even when properly done, it does not provide a full solution. It simply requires more resources on the part of the attacker.
- Architecture and Design: Ensure that protocols have specific limits of scale placed on them.
- Architecture and Design, Implementation: If the program must fail, ensure that it fails gracefully (fails closed). There may be a temptation to simply let the program fail poorly in cases such as low memory conditions, but an attacker may be able to assert control before the software has fully exited. Alternately, an uncontrolled failure could cause cascading problems with other downstream components; for example, the program could send a signal to a downstream process so the process immediately knows that a problem has occurred and has a better chance of recovery. Ensure that all failures in resource allocation place the system into a safe posture.
- Operation, Architecture and Design: Use quotas or other resource-limiting settings provided by the operating system or environment. For example, when managing system resources in POSIX, setrlimit() can be used to set limits for certain types of resources, and getrlimit() can determine how many resources are available. However, these functions are not available on all operating systems. When the current levels get close to the maximum that is defined for the application (see CWE-770), then limit the allocation of further resources to privileged users; alternately, begin releasing resources for less-privileged users. While this mitigation may protect the system from attack, it will not necessarily stop attackers from adversely impacting other users. Ensure that the application performs the appropriate error checks and error handling in case resources become unavailable (CWE-703).
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-8397-8qf6-f2gm vLLM: Phi-4 multimodal audio encoder materializes and retains full attention matrices: a single max-length audio OOM-crashes the vLLM engine | Medium | Also affects vllm |
| GHSA-x567-49g5-p4cf vLLM: Responses API bypasses trust_request_chat_template for chat_template_kwargs | Medium | Also affects vllm |
| GHSA-vvpg-rrww-wpmh vLLM: Permissive default CORS origins allow cross-origin requests to the vLLM HTTP server | Medium | Also affects vllm |
| GHSA-q97c-p368-75c4 vLLM: Out-of-vocabulary prompt token id passes input validation and triggers a CUDA device-side assert (engine crash / DoS) | Medium | Also affects vllm |
| GHSA-h3rc-j82p-2c3h vLLM: Untrusted EPS inputs can trigger denial of service or Ghostscript RCE | High | Also affects vllm |
| GHSA-gq5q-89c7-vhvx vLLM DeepSeek-OCR image processing vulnerable to denial of service | Medium | Also affects vllm |
| GHSA-6757-hggm-hp2w vLLM: Public multimodal SHM handle forgery reaches worker-side `pickle.loads()` with insecure serialization disabled | High | Also affects vllm |
| GHSA-542m-9234-8gvf vLLM: DoS via unbounded chat_template Jinja2 rendering exhausts the renderer thread pool | Medium | Also affects vllm |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor