Threat Intelligence

Live CVE feed

521 threats tracked across 12 launch stacks — sourced from NVD, GHSA, CISA KEV, OSV, npm Audit, and EPSS.

521threats · All threats· page 1/27
Get guardrails →

vLLM: Request-selected PyNvVideoCodec GPU decode bypasses static VRAM reservation

Summary Current vLLM main lets an inference request choose the PyNvVideoCodec GPU video decoder through media_io_kwargs.video.video_backend, but engine GPU memory reservation is computed only from static startup configuration and VLLM_VIDEO_LOADER_BACKEND. If the server starts with the default OpenCV/software backend and no --mm-ipc-gpu-memory-gb budget, a client can still route a video request into the PyNvVideoCodec path after startup, causing frontend CUDA-context, decoder-surface, and decoded-frame GPU allocations that were not carved out of the engine KV-cache budget. Technical Details The vulnerable boundary is the split between request-time media decoding choices in the API server and startup-time memory budgeting in the engine worker. Request bodies for Chat Completions and Responses expose media_io_kwargs, and those values are forwarded to the shared media connector. For video inputs, MediaConnector.fetch_video() copies self.media_io_kwargs["video"] into video_io_kwargs, only setting a model-derived backend when video_backend is absent. VideoMediaIO.__init__() then consumes video_backend from those kwargs and loads that backend from VIDEO_LOADER_REGISTRY. The relevant request-side source path is: ``python video_io_kwargs = dict(self.media_io_kwargs.get("video", {})) if "video_backend" not in video_io_kwargs and ( video_backend := get_video_loader_backend_for_processor(video_processor) ): video_io_kwargs["video_backend"] = video_backend video_io = VideoMediaIO(image_io, *video_io_kwargs) ` `python video_loader_backend = ( kwargs.pop("video_backend", None) or envs.VLLM_VIDEO_LOADER_BACKEND ) self.video_loader = VIDEO_LOADER_REGISTRY.load(video_loader_backend) ` VideoBackend.load_bytes() then dispatches backend == "pynvvideocodec" into decode_frames_pynvvideocodec(), which constructs a PyNvVideoCodec decoder, creates or uses a CUDA stream, reads stream metadata, decodes selected frames on the GPU, and copies those frames into pinned host memory. The new frontend GPU memory pool accounts only for raw decoded frame bytes when a pool exists; it does not make request-time backend selection safe when no startup reservation was made. The engine-side reservation code makes its decision from static model config and environment only: `python def _uses_pynvvideocodec_video_backend(mm_config) -> bool: video_kwargs = mm_config.media_io_kwargs.get("video", {}) video_loader_backend = ( video_kwargs.get("video_backend") or envs.VLLM_VIDEO_LOADER_BACKEND ) codec_backend = video_kwargs.get("backend") return ( video_loader_backend == PYNVVIDEOCODEC_VIDEO_BACKEND or codec_backend == PYNVVIDEOCODEC_VIDEO_BACKEND ) ` `python decoder_reserved_bytes = ( num_api_servers per_server_decoder_bytes if self._uses_pynvvideocodec_video_backend(mm_config) else 0 ) reserved_bytes = raw_frame_reserved_bytes + decoder_reserved_bytes if reserved_bytes <= 0: return available_kv_cache_memory_bytes ` With default static video configuration, mm_config.media_io_kwargs["video"] does not name PyNvVideoCodec and VLLM_VIDEO_LOADER_BACKEND defaults to OpenCV/software decoding. The worker therefore reserves no PyNv decoder/CUDA-context bytes. A later request can still set media_io_kwargs.video.video_backend="pynvvideocodec" and reach the GPU decoder path because that runtime field is intentionally honored by VideoMediaIO. PoV An ordinary multimodal inference request can carry the backend override in the request body: `json { "model": "served-vlm", "messages": [ { "role": "user", "content": [ {"type": "text", "text": "summarize this clip"}, {"type": "video_url", "video_url": {"url": "data:video/mp4;base64,<small-mp4>"}} ] } ], "media_io_kwargs": { "video": { "video_backend": "pynvvideocodec" } } } ` The following bounded source-level check confirms the code path without allocating GPU memory: `bash git clone --filter=blob:none https://github.com/vllm-project/vllm.git cd vllm git checkout ddd3855a28a561a5bb54d380c6e6b8b1e883cc4a python3 check_pynv_backend_reservation.py --repo . ` PoC The bounded check validates current source markers, simulates the exact static reservation predicate, and compares vulnerable and negative-control configurations. Key output: `json { "vulnerable": true, "head": "ddd3855a28a561a5bb54d380c6e6b8b1e883cc4a", "reservation_simulation": { "env_video_loader_backend": "opencv", "request_selects_pynv_after_startup": true, "vulnerable_static_reserved_bytes": 0, "negative_control_static_pynv_reserved_bytes": 2066953011, "raw_frame_only_control_reserved_bytes": 268435456, "unreserved_decoder_bytes_when_only_request_selects_pynv": 2066953011 } } ` The negative control is important: when PyNvVideoCodec is selected statically, the worker reserves 2066953011 bytes per API process for decoder surfaces plus CUDA context. The vulnerable case reserves 0 bytes for the same decoder overhead because PyNvVideoCodec is selected only by the later request. A second control with static OpenCV plus mm_ipc_gpu_memory_gb=0.25 reserves only the raw-frame semaphore budget and still does not reserve PyNv decoder/CUDA-context bytes. Impact An attacker who can submit video requests to a vLLM deployment with PyNvVideoCodec available can force frontend GPU decoding even when the engine did not reserve memory for that decoder during startup. On high-utilization serving deployments, the unreserved CUDA context, retained decoder surfaces, and decoded-frame allocations can reduce or exhaust GPU memory that the engine assumed was available for weights, activations, or KV cache, causing request failures, worker crashes, or service-level denial of service. Suggested severity is Medium with conservative CVSS v3.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H (6.5). If a deployment exposes the affected API without authentication, PR:N would raise the deployment-specific score. Suggested weaknesses are CWE-770 (Allocation of Resources Without Limits or Throttling) and CWE-400 (Uncontrolled Resource Consumption). This should not be rated Low because the affected resource is shared GPU memory in the serving path and the code already treats the PyNv decoder/CUDA-context footprint as large enough to reserve at startup when statically configured. Limitations: exploitation requires a GPU deployment where PyNvVideoCodec is installed and usable, and the request must reach a video-capable model/path. The issue does not claim code execution, data disclosure, or SSRF. Suggested Fix Do not allow untrusted request fields to select a GPU decoder that was not included in startup memory reservation. The simplest fix is to reject request-level media_io_kwargs.video.video_backend="pynvvideocodec" unless the static server configuration already selected PyNvVideoCodec and reserved its decoder/CUDA-context budget. If dynamic backend selection remains supported, split software and GPU decoder policies: allow request selection among CPU/software decoders only, require an explicit operator allowlist for GPU decoders, and include every request-selectable GPU decoder in the startup reservation predicate. Add regression coverage for static OpenCV startup config plus request-level PyNvVideoCodec override, and preserve the negative control where static PyNvVideoCodec configuration reserves decoder/CUDA-context bytes. Affected Package/Versions Package: vllm from vllm-project/vllm. Confirmed affected: current main at ddd3855a28a561a5bb54d380c6e6b8b1e883cc4a. Introduced by: af16446bf39de047ab57649c933063cf1cbf1e50, Vram semaphore infra (#44465), committed 2026-06-26T17:32:51-07:00. Release status checked: git tag --contains af16446bf returned no release tags in the fresh checkout. GitHub repository metadata reported latest published release v0.23.0 published 2026-06-15T05:27:20Z; the local v0.24.0 tag also does not contain the introducing commit. The affected range should therefore be current main builds containing af16446bf until fixed, rather than a confirmed released-version range. Advisory History Public vLLM advisories checked included audio decompression-bomb DoS, unbounded video/jpeg frame-count DoS, MediaConnector SSRF, video processing RCE, multimodal embedding DoS/RCE, GGUF GPU memory exposure, multimodal hashing, and other request-parameter DoS classes. None matched request-selected PyNvVideoCodec or the static VRAM reservation mismatch. Prior local/private vLLM report families checked included request-level media_io_kwargs reopening video/jpeg frame fanout, GLM video metadata amplification, and audio media decode duration-limit bypass. Those reports share the request-level media kwargs boundary, but they target CPU/media decode limits or model metadata amplification. This report targets a different privileged asset and fix surface: GPU decoder selection after engine startup memory reservation. Focused GitHub issue/PR searches for pynvvideocodec, mm_ipc_gpu_memory, video_backend media_io_kwargs, Vram semaphore infra, and frontend multimodal GPU decoding found the PyNvVideoCodec zero-copy RFC, an old do-not-review prototype, merged PR #44465, and an unrelated TorchCodec backend PR. No public issue or PR described this security boundary. Appendix: Bounded Source-Level Check `python #!/usr/bin/env python3 from __future__ import annotations import argparse import json import re import subprocess from pathlib import Path MIB = 1024 1024 GIB = 1024 MIB def read(repo: Path, rel: str) -> str: return (repo / rel).read_text(encoding="utf-8") def const_int(source: str, name: str) -> int: expr = re.search(rf"^{name}\s=\s(.+)$", source, flags=re.MULTILINE).group(1).strip() if expr == "128 MiB_bytes": return 128 MIB if expr == "int(1.8 1024 MiB_bytes)": return int(1.8 1024 MIB) if expr == "1": return 1 raise AssertionError(expr) def uses_pynv_static(static_media_io_kwargs: dict[str, dict[str, str]], env_backend: str) -> bool: video_kwargs = static_media_io_kwargs.get("video", {}) video_loader_backend = video_kwargs.get("video_backend") or env_backend codec_backend = video_kwargs.get("backend") return video_loader_backend == "pynvvideocodec" or codec_backend == "pynvvideocodec" def reserve_bytes(static_media_io_kwargs, env_backend, mm_ipc_gpu_memory_gb, decoder_bytes, cuda_context_bytes, retained_decoders): raw_frame_reserved_bytes = int(mm_ipc_gpu_memory_gb GIB) per_server_decoder_bytes = decoder_bytes retained_decoders + cuda_context_bytes decoder_reserved_bytes = per_server_decoder_bytes if uses_pynv_static(static_media_io_kwargs, env_backend) else 0 return raw_frame_reserved_bytes + decoder_reserved_bytes parser = argparse.ArgumentParser() parser.add_argument("--repo", required=True, type=Path) repo = parser.parse_args().repo.resolve() media_video = read(repo, "vllm/multimodal/media/video.py") connector = read(repo, "vllm/multimodal/media/connector.py") chat_protocol = read(repo, "vllm/entrypoints/openai/chat_completion/protocol.py") responses_protocol = read(repo, "vllm/entrypoints/openai/responses/protocol.py") gpu_worker = read(repo, "vllm/v1/worker/gpu_worker.py") video_core = read(repo, "vllm/multimodal/video.py") assert "media_io_kwargs: dict[str, dict[str, Any]] | None = Field(" in chat_protocol assert "media_io_kwargs: dict[str, dict[str, Any]] | None = Field(" in responses_protocol assert 'video_io_kwargs = dict(self.media_io_kwargs.get("video", {}))' in connector assert 'if "video_backend" not in video_io_kwargs and (' in connector assert 'kwargs.pop("video_backend", None) or envs.VLLM_VIDEO_LOADER_BACKEND' in media_video assert "elif backend == PYNVVIDEOCODEC_VIDEO_BACKEND:" in video_core assert 'video_kwargs = mm_config.media_io_kwargs.get("video", {})' in gpu_worker decoder_bytes = const_int(video_core, "PYNVVIDEOCODEC_DECODER_GPU_MEMORY_BYTES") retained_decoders = const_int(video_core, "PYNVVIDEOCODEC_MAX_RETAINED_DECODERS") cuda_context_bytes = const_int(video_core, "PYNVVIDEOCODEC_CUDA_CONTEXT_BYTES") per_server_decoder_bytes = decoder_bytes * retained_decoders + cuda_context_bytes vulnerable_static_reserved = reserve_bytes({}, "opencv", 0.0, decoder_bytes, cuda_context_bytes, retained_decoders) negative_control_reserved = reserve_bytes({"video": {"video_backend": "pynvvideocodec"}}, "opencv", 0.0, decoder_bytes, cuda_context_bytes, retained_decoders) raw_frame_only_control = reserve_bytes({}, "opencv", 0.25, decoder_bytes, cuda_context_bytes, retained_decoders) head = subprocess.check_output(["git", "-C", str(repo), "rev-parse", "HEAD"], text=True).strip() print(json.dumps({ "head": head, "vulnerable": vulnerable_static_reserved == 0 and negative_control_reserved == per_server_decoder_bytes, "reservation_simulation": { "env_video_loader_backend": "opencv", "request_selects_pynv_after_startup": True, "vulnerable_static_reserved_bytes": vulnerable_static_reserved, "negative_control_static_pynv_reserved_bytes": negative_control_reserved, "raw_frame_only_control_reserved_bytes": raw_frame_only_control, "unreserved_decoder_bytes_when_only_request_selects_pynv": per_server_decoder_bytes, }, }, indent=2, sort_keys=True)) ``

OWASP A06LLM04 · Model DoSOWASP LLM
Get guardrail →

vLLM: Unauthenticated audio decompression-bomb DoS in /v1/chat/completions

Summary The audio decode-duration guard (max_duration_s, env VLLM_MAX_AUDIO_DECODE_DURATION_S, 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, input_audio 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.load_base64) 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: load_audio_pyav — metadata reject (~82-98) and live sample-count reject (~129-136) load_audio_soundfile — frames reject (~165-174) All are gated on if max_duration_s is not None. It is passed in exactly one place — the transcription serving layer: ``python .../speech_to_text/base/serving.py:~170-174 load_audio(buf, sr=..., max_duration_s=self.max_audio_decode_duration_s) self.max_audio_decode_duration_s = envs.VLLM_MAX_AUDIO_DECODE_DURATION_S (default 600) ` The chat path never threads it: `python vllm/multimodal/media/audio.py:237-238 def load_bytes(self, data: bytes) -> tuple[npt.NDArray, float]: return load_audio(BytesIO(data), sr=None) # no max_duration_s -> every guard above is skipped ` Unauthenticated reachability chain (chat): parse_input_audio (chat_utils.py) -> parse_audio -> connector.fetch_audio -> AudioMediaIO._load_data_url -> load_base64 -> load_bytes -> load_audio(..., sr=None). The connector never passes max_duration_s, and inline data: URLs need no HTTP fetch (so VLLM_AUDIO_FETCH_TIMEOUT does not bound them). The OpenAI-compatible server has no auth by default (auth only when --api-key / VLLM_API_KEY` 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

multer vulnerable to Denial of Service via file descriptor leak on aborted uploads

Impact A vulnerability in multer 2.2.0 allows an attacker to trigger a Denial of Service (DoS) by aborting or truncating multipart uploads. When using diskStorage, the destination write stream is not closed if the upload is aborted before it finishes, so each failed request leaks an open file descriptor and retains its disk blocks until the process exits. Repeated failed uploads can exhaust the available file descriptors. All applications using multer's disk storage are affected. Patches Users should upgrade to 2.3.0. Workarounds None.

OWASP A06LLM04 · Model DoSOWASP Web
Get guardrail →

vLLM: SSRF + arbitrary local file read in MiMoV2OmniMultiModalProcessor `_fetch_image` and audio loader bypass MediaConnector protections

Summary vllm/transformers_utils/processors/mimo_v2_omni.py — the multimodal processor for MiMoV2OmniForCausalLM — issues requests.get(...) directly on user-supplied image and audio URL strings and Image.open(...) on user-supplied local paths, without the SSRF / allowed_local_media_path checks that vllm.multimodal.utils.MediaConnector was hardened with in GHSA-qh4c-xf7m-gxfc, GHSA-v359-jj2v-j536, and GHSA-pf3h-qjgv-vcpr. This is the same bug class as those three published advisories, in a code path the patches missed. When a user passes a URL or local-file string through multi_modal_data (e.g. LLM.generate(multi_modal_data={"image": "http://..."})), the processor takes the unsanitized string and dispatches it without any URL-scheme allowlist, network-target allowlist, size cap, or local-path allowlist. Details File: vllm/transformers_utils/processors/mimo_v2_omni.py (current main) Sink 1 — image SSRF + local-file read (_fetch_image, lines 231–249): ``python def _fetch_image(src: Any) -> Image.Image: if isinstance(src, Image.Image): return _to_rgb(src) if isinstance(src, bytes): return _to_rgb(copy.deepcopy(Image.open(BytesIO(src)))) if isinstance(src, str): if src.startswith(("http://", "https://")): r = requests.get(src, timeout=30) # SSRF: no allowlist, follows redirects r.raise_for_status() return _to_rgb(copy.deepcopy(Image.open(BytesIO(r.content)))) if src.startswith("file://"): return _to_rgb(Image.open(src[7:])) # arbitrary local file read if src.startswith("data:image"): ... return _to_rgb(Image.open(src)) # fallback also opens local files raise ValueError(f"Unrecognized image source: {type(src)}") ` Sink 2 — audio SSRF (around line 471): `python elif audio.startswith(("http://", "https://")): r = requests.get(audio, timeout=30) # SSRF: same pattern r.raise_for_status() file_obj = io.BytesIO(r.content) ` Reachability. _fetch_image is invoked from MiMoVLProcessor.process_image: `python def process_image(self, image: ImageInput) -> torch.Tensor: kw = self._resolve_img_kw(image) src = image.image if isinstance(src, (str, bytes)): src = _fetch_image(src) ... ` MiMoVLProcessor is wrapped by MiMoV2OmniMultiModalProcessor and registered for the MiMoV2OmniForCausalLM model architecture (vllm/model_executor/models/mimo_v2_omni.py:1169). Whenever a user passes a string into multi_modal_data["image"] (or ["audio"]) for this model, the unsanitized URL/path reaches the sink. Comparison to the recent fixes. The remediation pattern adopted in the three earlier advisories was to route every external resource fetch through MediaConnector, which checks allowed_local_media_path and applies SSRF protection before issuing the network request. chat_utils.py (lines 838, 902, 924, 963, 1053, 1081) already uses self._connector.fetch_image / fetch_audio / fetch_video. The model processor in mimo_v2_omni.py was added later and skipped the connector — it calls requests.get and Image.open directly. Result: the public OpenAI chat-completion path is protected, but library use (LLM.generate(multi_modal_data=...)), batch processing, and any other path that lets a string reach the processor receive no protection. Impact 1. SSRF — internal-network probing / cloud-metadata theft. Standard requests.get follows redirects and accepts any URL. An attacker who controls a multi_modal_data value can: read AWS / GCP / Azure instance metadata (e.g. http://169.254.169.254/latest/meta-data/iam/security-credentials/), probe internal services on the vLLM host (http://127.0.0.1:<port>, http://10.x.y.z), exfiltrate via DNS / HTTP timing oracles even when the body is rejected by Image.open. 2. Arbitrary local file read via file://path (line 242) and the unguarded fallback Image.open(src) (line 248). Any file readable by the vLLM process is reachable through the model pipeline; with suitable formats this exposes /etc/passwd, ~/.aws/credentials, etc. 3. Server-side traffic generation / amplification by hammering arbitrary URLs from the vLLM host, with a 30-second timeout per request. Suggested remediation Replace direct requests.get and bare Image.open paths with MediaConnector.fetch_image / fetch_audio_async (or pass the inputs through MediaConnector before they reach the processor): `python vllm/transformers_utils/processors/mimo_v2_omni.py from vllm.multimodal.utils import MediaConnector _connector = MediaConnector() def _fetch_image(src): if isinstance(src, Image.Image): return _to_rgb(src) if isinstance(src, bytes): return _to_rgb(copy.deepcopy(Image.open(BytesIO(src)))) if isinstance(src, str): return _to_rgb(_connector.fetch_image(src)) # delegates to the hardened path raise ValueError(f"Unrecognized image source: {type(src)}") ` Same change for the audio loader at line 471. This re-uses the SSRF allowlist, allowed_local_media_path policy, and size caps that the previous patches added. Alternative: forbid str src from reaching the processor and require all multi-modal pre-processing to go through chat_utils.py / MediaConnector before hitting the model. Larger surface change, but completes the architectural fix. Discovery Static review on vllm@main (HEAD as of 2026-04-30) — found by triaging the file list against the three recent SSRF advisories: the mimo_v2_omni.py processor, added after those fixes, reintroduced the same bypass class. Reporter Ievgen Bondarenko — sactransport2000@gmail.com — GitHub @ibondarenko1`

OWASP A10LLM02 · Insecure OutputOWASP LLM
Get guardrail →

vLLM: Cross-User Data Leak Vulnerability

Summary An integer overflow in the act_and_mul_kernel kernel can cause the output of one user request to be incorporated into the response of another request within the same inference batch. Under certain conditions, the last request in a batch can receive a partial or complete copy of the first user's inference result, resulting in cross-user data leakage. Details The root cause is an integer overflow in the expression blockIdx.x 2 d at https://github.com/vllm-project/vllm/blob/ff712f6447093d07747c88680b9d006b119f5890/csrc/activation_kernels.cu#L82. As a result, the computation for one user (User A) can incorrectly consume input data from another user (User B). In particular, when 2^32 is divisible by d, the overflow can cause User A's output to contain portions of User B's inference result. In some cases, User B's response may be copied entirely into User A's response. This constitutes a severe cross-user information disclosure vulnerability and is straightforward to trigger. PoC We reproduced the issue using meta-llama/Llama-3.2-1B-Instruct, for which d = 8192. Using the following configuration: Batch size: 17 Sequence length: 16384 The final response in the batch becomes an exact copy of the first response in the batch, demonstrating complete cross-user data leakage. Impact This vulnerability enables cross-user information disclosure. An attacker can intentionally craft requests that are processed within the same inference batch as a victim's request and cause the victim's inference output to be copied into the attacker's response. As a result, sensitive information contained in another user's model response may be exposed to an unauthorized party. Versions For versions prior and equal to 0.21.0, the bug is in csrc/activation_kernels.cu, and for versions later than 0.21.0, the bug is in csrc/libtorch_stable/activation_kernels.cu.

vLLM: Incomplete CVE-2025-62164 remediation can be bypassed by concurrent prompt parts

Executive Summary The follow-up protection for CVE-2025-62164 is incomplete at vLLM revision 26587f9519e22a5c4549ead7595ad9ca3229c4fd. It wraps serialized prompt-embedding reconstruction and dense conversion in torch.sparse.check_sparse_tensor_invariants(), but PyTorch 2.11.0 implements that context with save/enable/restore operations over process-global state. Two prompt-embedding parts in one /v1/chat/completions request are gathered concurrently on the event loop's default executor. When one context exits before the other loads its tensor, it can restore the global flag to False while the second part remains inside its guard. In a deterministic run against hash-verified source from the affected revision, the actual target loader rejected an invalid sparse payload as a negative control. The frozen chat tracker then scheduled benign and malicious parts on distinct asyncio_0 and asyncio_1 threads. The benign context exited, the malicious loader observed the invariant flag disabled, and torch.load(weights_only=True) reconstructed indices [[10], [10]] for a declared shape of [3, 3]. The run intercepted the target's to_dense() call before it operated on the invalid tensor. This primary trigger requires --enable-prompt-embeds, which is default-off, but it does not require renderer_num_workers > 1, a multimodal model, or --enable-mm-embeds. API authentication is optional in the stock server: middleware is installed only when CLI or environment API keys are supplied. The lab proves bypass of the follow-up guard, invalid sparse reconstruction, and guarded-sink reachability. Crash and memory-corruption consequences are conditional on the behavior documented by the published CVE. Background CVE-2025-62164 / GHSA-mrw7-hf4f-83pf concerns client-controlled serialized prompt_embeds reaching torch.load(weights_only=True) and an invalid sparse tensor reaching to_dense(). The advisory attributes memory corruption, denial of service, and potential code execution to that historical unsafe operation. The remediation chronology matters for duplicate handling: PR #27204, merge commit 58fab50d82838d5014f4a14d991fdb9352c9c84b on 2025-10-22, introduced the default-off enable_prompt_embeds gate. It did not add the sparse-invariant context. Commit 84e23d103d3483f944780d0d42bcf0993fd27e3a on 2025-12-15, titled additional protection for CVE-2025-62164 (#30649), added the process-global sparse-invariant context around load, type check, and dense conversion. Refactor commit f0a1c8453ad1c664c8a04c83fe545195fcd556eb on 2026-01-31 moved the guarded loader into vllm/renderers/embed_utils.py while preserving the same context. Chat content-part commit 14043dfecd35dd2f12b4d51eb9fa166184a0ca0f on 2026-05-01 introduced prompt_embeds chat parts and the concurrent one-request schedule described here. This report therefore does not present the malformed sparse payload or to_dense() sink as new. It reports a distinct concurrency root cause and trigger: unsynchronized save/enable/restore of the process-global follow-up guard, reachable through the later multi-part chat scheduler. The affected revision pins PyTorch 2.11.0 in pyproject.toml:10. Vulnerability Details The target's safe_load_prompt_embeds performs the guarded operation in vllm/renderers/embed_utils.py:16-39: ``python with torch.sparse.check_sparse_tensor_invariants(): tensor = torch.load( BytesIO(pybase64.b64decode(embed, validate=True)), weights_only=True, map_location=torch.device("cpu"), ) if not isinstance(tensor, torch.Tensor): raise VLLMValidationError(...) tensor = tensor.to_dense() ` The context is not request-local. With the global flag initially disabled, we can describe the verified interleaving: 1. Benign part A enters, saves False, and enables the flag. 2. Malicious part B enters, saves True, and leaves the flag enabled. 3. A completes its load and exits, restoring its saved False value. 4. B remains lexically inside its context but observes the actual global flag as False. 5. B's torch.load(..., weights_only=True) reconstructs the malformed sparse tensor. 6. The target reaches tensor.to_dense() before later rank, hidden-size, and dtype checks. weights_only=True constrains deserialization types; it does not compensate for a sparse invariant check that another request has disabled. The complete stock actor-to-sink chain, traced in the affected source, is: POST /v1/chat/completions (vllm/entrypoints/openai/chat_completion/api_router.py:41-61) -> OpenAIServingChat.create_chat_completion -> _create_chat_completion -> render_chat_request (vllm/entrypoints/openai/chat_completion/serving.py:206-280) -> OnlineRenderer.render_chat (vllm/renderers/online_renderer.py:95-190) -> preprocess_chat (vllm/renderers/online_renderer.py:335-380) -> BaseRenderer.render_chat_async (vllm/renderers/base.py:1070-1105) -> HfRenderer.render_messages_async (vllm/renderers/hf.py:1049-1085) -> parse_chat_messages_async (vllm/entrypoints/chat_utils.py:1911-1945) -> content-part parse_prompt_embeds and _load_prompt_embeds_async (vllm/entrypoints/chat_utils.py:1099-1120) -> AsyncMultiModalItemTracker.resolve_items (vllm/entrypoints/chat_utils.py:818-835) -> asyncio.gather of both prompt parts -> safe_load_prompt_embeds_async -> make_async -> loop.run_in_executor(executor=None, ...) (vllm/utils/async_utils.py:28-45) -> guarded torch.load -> to_dense(). The prompt async helper is created without an explicit executor, so it uses the event loop's default executor. This path is separate from the renderer's configurable pool. The deterministic scheduler run observed the two parts on distinct default-executor threads while leaving renderer_num_workers at its default of one. prompt_embeds bypasses multimodal processing, and the tracker explicitly permits it when is_multimodal_model=False (vllm/entrypoints/chat_utils.py:793-837). Consequently, the primary trigger needs neither a multimodal model nor enable_mm_embeds. The source also states that async wrappers must be thread-safe (vllm/utils/async_utils.py:28-38), while a target test acknowledges that the sparse flag is not thread-local and concurrent users can leak state (tests/renderers/test_sparse_tensor_validation.py:58-61). Exploitability Analysis The following evidence labels separate what was demonstrated from what remains conditional: | Label | Claim | | --- | --- | | Verified by run | PyTorch 2.11.0 rejects the identical invalid payload through the actual target loader without the race. | | Verified by run | The hash-verified frozen tracker schedules two prompt parts on distinct default-executor threads, races the flag to False, reconstructs the invalid sparse tensor, and reaches the target to_dense() call while the interception prevents execution. | | Traced in source | A client can supply multiple prompt_embeds content parts through the stock /v1/chat/completions route and the function chain above. | | Traced in source | enable_prompt_embeds defaults to False (vllm/config/model.py:255-260), so the operator must opt in. enable_mm_embeds and non-default renderer workers are not preconditions for this path. | | Traced in source | api_key defaults to None (vllm/entrypoints/openai/cli_args.py:264), and authentication middleware is installed only when a CLI or environment key is present (vllm/entrypoints/openai/api_server.py:306-310). With a configured key, the attacker must authenticate; without one, the stock route has no API-key middleware. | | Unrun | A live HTTP/GPU server, real-world race win rate, unsafe dense conversion, process crash, memory corruption, and reliable code execution. | The feature is documented for trusted users, which narrows intended exposure. It is not a memory-safety boundary: a user authorized to submit embedding inputs should not be able to disable a process-wide invariant for concurrent work. The current run proves the same invalid sparse object can cross the guard and reach the historical sink. If executing that sink retains the behavior described in CVE-2025-62164 for the deployed PyTorch build, denial of service or memory corruption may follow. This is a conditional impact statement, not a reproduced outcome. Reliable RCE is not claimed. The opt-in feature, scheduling requirement, and absence of a measured live win rate support Medium/P2 despite the serious historical sink class. No additional deployment assumptions are required for the one-request scheduler beyond stock default-executor concurrency being available. Remediation The immediate fix is one shared process-wide lock around every use of this process-global sparse guard. The lock must cover invariant enabling, deserialization, tensor type validation, and dense conversion: `python with shared_sparse_load_lock: with torch.sparse.check_sparse_tensor_invariants(): tensor = torch.load(..., weights_only=True, map_location="cpu") validate_tensor_type(tensor) tensor = tensor.to_dense() ` Every prompt, image, and audio loader that manipulates the same global flag must use the same lock. A lock only around torch.load, separate per-loader locks, or a lock omitted from the chat helper would leave overlapping save/restore sequences possible. The stronger design is to avoid mutable process-global validation state in concurrent request code. Prefer a PyTorch per-call invariant check if one is available, or reconstruct and validate serialized embeddings inside a deliberately serialized boundary before any sparse operation. Regression coverage should: Preserve the actual-target negative control using the identical malformed payload. Force A-enter, B-enter, A-exit, B-load and assert B remains protected. Execute the multi-part chat tracker with the event loop's default executor and renderer_num_workers=1. Cover cross-loader overlap so later prompt, image, or audio changes cannot bypass a shared fix. Assert the global flag is restored after success and exceptions. Reject invalid tensors before any dense conversion. Until a fix is deployed, leaving enable_prompt_embeds disabled removes this stock source path. Summary The affected vLLM revision uses a process-global PyTorch context as the follow-up protection for CVE-2025-62164. A later chat feature causes two prompt-embedding parts from one request to run concurrently on the default executor. One context can restore the flag to False while the other is still guarded, allowing the historical malformed sparse payload class to reach the historical to_dense()` sink. The new issue is the concurrent guard bypass and shipped trigger, not the payload or sink. Runtime validation proves the bypass and safe sink reachability on PyTorch 2.11.0; historical crash and memory-corruption effects remain conditional, and RCE was not tested or claimed.

vLLM: ReDoS via structured_outputs.regex in the lm-format-enforcer backend (no compile timeout) — missed sibling of GHSA-rwxx-mrjm-wc2m

Summary The fix for GHSA-rwxx-mrjm-wc2m ("ReDoS via structured_outputs.regex compiled without timeout") wrapped the regex compile in the xgrammar and outlines backends with compile_regex_with_timeout (and, for outlines, validate_regex_is_buildable). 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/structured_output/backend_lm_format_enforcer.py: line 110: character_level_parser = lmformatenforcer.RegexParser(grammar_spec) — builds an interegular FSM from the attacker regex synchronously, no timeout. line 155: validate_structured_output_request_lm_format_enforcer returns immediately on if so_params.regex: — no validation. Sibling backends that WERE patched by GHSA-rwxx: backend_xgrammar.py:92 → compile_regex_with_timeout(...). backend_outlines.py:65 → compile_regex_with_timeout(...) (plus validate_regex_is_buildable). 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 {"structured_outputs":{"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 compile_regex_with_timeout 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 (backend_lm_format_enforcer.py:110) through the same compile_regex_with_timeout guard already applied to the xgrammar and outlines backends, and reject un-buildable / oversized patterns in validate_structured_output_request_lm_format_enforcer`.

OWASP A06LLM04 · Model DoSOWASP LLM
Get guardrail →

vLLM: Unauthenticated Internal Path and Username Disclosure via Validation Error Messages

Summary When the vLLM API receives a malformed request (e.g., invalid JSON or missing required fields), FastAPI raises a Pydantic RequestValidationError. The validation_exception_handler in vllm/entrypoints/openai/server_utils.py converts this exception to a string via str(exc), which includes the internal file path and line number of the handler function. The existing sanitize_message() function in vllm/entrypoints/utils.py strips memory addresses (e.g., 0x7f...) but does not strip File "...", line X patterns. The result is a user-facing HTTP response that leaks internal system information. Impact An unauthenticated attacker can extract the following with a single malformed request: OS username running the vLLM process (e.g., ubuntu) Home directory path (e.g., /home/ubuntu/) Virtual environment path (e.g., vllm-env/) Python version (e.g., 3.12) Internal package structure and line numbers (e.g., vllm/entrypoints/openai/chat_completion/api_router.py) Handler function names per endpoint, enabling precise version fingerprinting This information aids attackers in constructing targeted exploits: environment paths narrow the attack surface, and handler function names + line numbers enable exact version identification even when the /version endpoint is disabled. All POST endpoints that accept JSON bodies are affected, including /v1/chat/completions, /v1/completions, /tokenize, and /detokenize. Workarounds Deploying vLLM behind a reverse proxy that rewrites error response bodies to strip file paths would mitigate this, though it is fragile. Remediation Recommendation Two possible fixes (either suffices): Option A — Fix validation_exception_handler: Construct the error message from exc.errors() (the structured Pydantic error list) rather than str(exc). This avoids the traceback-style string entirely. Option B — Fix sanitize_message: Add a regex to strip File "...", line \d+ patterns, similar to how memory addresses are already stripped: ``python import re msg = re.sub(r'File ".*?", line \d+, in \w+', '[internal]', msg) `` Option A is preferred as it addresses the root cause rather than filtering symptoms. Environment Tested vLLM 0.20.1 (pip install, latest stable as of May 2026) Python 3.12 Ubuntu 22.04 Model: Qwen/Qwen2-0.5B (text-only; bug is model-independent) This was fixed here: https://github.com/vllm-project/vllm/commit/e87521626f

vLLM: Derender endpoints decode caller-supplied GenerateResponse token IDs without output bounds

Summary The /v1/completions/derender and /v1/chat/completions/derender endpoints accept caller-supplied GenerateResponse objects and postprocess every nested choices[].token_ids list directly. Unlike the normal render/generate path, derender does not enforce model context length, resolved max_tokens, max_num_seqs, choice-count, or response-size bounds before detokenizing and returning the supplied token IDs. An authenticated API client can therefore make the CPU-only render frontend, or any server exposing these /v1 derender routes, spend CPU and memory proportional to attacker-chosen generated-output-shaped JSON rather than to a bounded generation result. Technical Details The render router registers /v1/chat/completions/derender and /v1/completions/derender in vllm/entrypoints/serve/render/api_router.py, and the OpenAI API server attaches this router whenever "generate" or "render" is in supported_tasks (vllm/entrypoints/openai/api_server.py). The routes are under /v1, so they are part of the OpenAI-compatible HTTP API surface and are protected by the API-key middleware when --api-key is configured. The request types trust generated-output-shaped data from the client. In vllm/entrypoints/serve/disagg/protocol.py, GenerateResponseChoice accepts token_ids: list[int] | None = None, GenerateResponse accepts choices: list[GenerateResponseChoice], and DerenderCompletionRequest accepts generate_responses: list[GenerateResponse]. These fields have no max length, max item count, or relationship to a prior GenerateRequest. The sink is OnlineDerenderer. derender_completion() iterates every supplied generate_responses entry and every nested choice, calls tokenizer.decode(choice.token_ids, skip_special_tokens=True), appends the decoded text to the response choices, and increments total_completion_tokens from the same supplied list length. derender_chat() has the same shape for a single supplied generate_response, and can also feed the decoded text into tool/reasoning parsers when a parser and chat_request are present. ServingRender.derender_completion_response() calls online_derenderer.derender_completion(request.generate_responses, request.prompt_tokens) before applying any completion-level validation beyond the model check. Normal render and generation paths derive output limits from max_model_len, the rendered prompt length, request max_tokens / max_completion_tokens, and scheduler limits. Derender bypasses that invariant because it accepts the already-generated output shape directly from the HTTP caller. The missing invariant is: derender should only postprocess bounded generated output, and client-supplied derender payloads must be rejected if their nested generated token/logprob structures exceed the same limits that generation would have enforced. PoV The following bounded PoV can be run from a current vLLM checkout containing PR #43606. It asserts the current source facts for the derender routes, unchecked request fields, and decode sink, then simulates the same derender loop with a counting tokenizer. The negative control is a one-choice, 32-token response. The amplified payload keeps the test bounded but demonstrates that all decoded work and returned text scale directly with caller-supplied GenerateResponse contents. ``python #!/usr/bin/env python3 import subprocess from dataclasses import dataclass from pathlib import Path SOURCE = Path(".") def require_source_fact(path: str, needles: list[str]) -> None: text = (SOURCE / path).read_text() missing = [needle for needle in needles if needle not in text] if missing: raise AssertionError(f"{path} missing expected facts: {missing}") def source_head() -> str: return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=SOURCE, text=True).strip() @dataclass class Choice: index: int token_ids: list[int] @dataclass class GenerateResponse: request_id: str choices: list[Choice] class CountingTokenizer: def __init__(self) -> None: self.decode_calls = 0 self.decoded_ids = 0 def decode(self, token_ids: list[int], , skip_special_tokens: bool = True) -> str: self.decode_calls += 1 self.decoded_ids += len(token_ids) return "x" len(token_ids) def derender_completion_like_current_head(generate_responses: list[GenerateResponse], tokenizer: CountingTokenizer) -> tuple[int, int, int]: output_chars = 0 choices = 0 total_completion_tokens = 0 for gen in generate_responses: for choice in gen.choices: if not choice.token_ids: raise ValueError("choice has empty or null token_ids") decoded_text = tokenizer.decode(choice.token_ids, skip_special_tokens=True) output_chars += len(decoded_text) total_completion_tokens += len(choice.token_ids) choices += 1 return choices, total_completion_tokens, output_chars def make_payload(responses: int, choices_per_response: int, tokens_per_choice: int) -> list[GenerateResponse]: token_ids = [42] tokens_per_choice return [GenerateResponse(request_id=f"gen-{r}", choices=[Choice(index=c, token_ids=list(token_ids)) for c in range(choices_per_response)]) for r in range(responses)] def run_case(name: str, payload: list[GenerateResponse]) -> None: tokenizer = CountingTokenizer() choices, completion_tokens, output_chars = derender_completion_like_current_head(payload, tokenizer) print(f"{name}: responses={len(payload)} choices={choices} decode_calls={tokenizer.decode_calls} decoded_token_ids={tokenizer.decoded_ids} completion_tokens={completion_tokens} output_chars={output_chars}") require_source_fact("vllm/entrypoints/serve/render/api_router.py", ['"/v1/completions/derender"', '"/v1/chat/completions/derender"', "app.include_router(router)"]) require_source_fact("vllm/entrypoints/serve/disagg/protocol.py", ["class GenerateResponseChoice(BaseModel):", "token_ids: list[int] | None = None", "class GenerateResponse(BaseModel):", "choices: list[GenerateResponseChoice]", "class DerenderCompletionRequest(BaseModel):", "generate_responses: list[GenerateResponse]"]) require_source_fact("vllm/renderers/online_derenderer.py", ["async def derender_completion(", "for gen, pt in zip(generate_responses, prompt_tokens_list):", "for choice in gen.choices:", "decoded_text = tokenizer.decode(", "total_completion_tokens += len(choice.token_ids)"]) print("source_checks=ok") print(f"source_head={source_head()}") run_case("negative_control", make_payload(responses=1, choices_per_response=1, tokens_per_choice=32)) run_case("amplified_payload", make_payload(responses=16, choices_per_response=4, tokens_per_choice=8192)) print("observation=derender decodes every caller-supplied token id before any max_model_len, max_tokens, max_num_seqs, or response-size check") ` Impact An attacker with access to the /v1 API can send derender requests that consume CPU and memory in the frontend/postprocessing process and can cause large responses unrelated to any bounded generation. In disaggregated deployments, this affects the CPU-only render frontend; in servers where the render router is attached alongside generation, it affects the same OpenAI-compatible server process that handles normal client traffic. This can degrade availability for other clients sharing the process. Likely CWE: CWE-400 (Uncontrolled Resource Consumption) / CWE-770 (Allocation of Resources Without Limits or Throttling). Conservative CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L (4.3). This is not Low severity because a regular network API client can induce availability impact in a shared service without local access, invalid model artifacts, or special runtime privileges. If the server is deployed without API-key enforcement for /v1, the privileges component becomes PR:N. Suggested Fix Validate derender payloads before any detokenization or parser invocation. Apply bounded limits to generate_response(s), choices, token_ids, prompt_logprobs, logprobs.content, top_logprobs, and routed_experts that are at least as strict as the corresponding generation-side limits. For completions, reject generate_responses counts above the number of prompts that /v1/completions/render would have produced, and reject total nested choice counts above max_num_seqs / n limits. For each choice, reject token_ids longer than the resolved output-token budget, or require derender callers to submit the original bounded GenerateRequest / sampling metadata and validate the GenerateResponse against it before decoding. Add regression tests for both derender endpoints. The tests should show that a normal bounded derender payload succeeds, while oversized generate_responses, oversized choices, oversized token_ids, and oversized logprob/top-logprob structures are rejected before tokenizer.decode() or parser execution. Affected Package/Versions Confirmed affected: current main at ddd3855a28a561a5bb54d380c6e6b8b1e883cc4a and downstream/nightly builds that include the derender endpoints introduced by PR #43606. The derender router, request models, decode sink, render serving bridge, and OpenAI API router attachment have no relevant diff from 00e045b7c7b82599f626779e111233abd4d0a64e to ddd3855a28a561a5bb54d380c6e6b8b1e883cc4a. Latest release checked: v0.23.0, published on 2026-06-15. Its vllm/entrypoints/serve/render/api_router.py does not expose /v1/completions/derender or /v1/chat/completions/derender, so v0.23.0 was not confirmed affected. Advisory History PR #43606 ("[Render] Add /derender endpoints for disaggregated postprocessing") introduced the derender endpoints on main. PR #44285 later refactored the render serving code, and current head still contains the unchecked derender flow. Public issue search for derender GenerateResponse token_ids returned no reports. Public search for "/v1/completions/derender" returned the derender feature RFC #42729 and unrelated bugs, but no size-bound, DoS, or generated-output postprocessing issue. Related public request-fanout and resource-bound advisories are distinct: GHSA-3mwp-wvh9-7528 covers an unbounded n parameter on the normal OpenAI completion/chat generation routes. Its root cause is missing upper-bound validation for generated sequence count, its sink is request fanout and request-object copying into the async engine path before scheduling, its precondition is a caller-controlled n, and its fix surface is a cap on generated sequence count. This report reaches /v1/completions/derender and /v1/chat/completions/derender, not the normal generate routes; its root cause is unchecked caller-supplied GenerateResponse / choices / token_ids structures, its sink is OnlineDerenderer detokenization and response construction after generation, its precondition is access to the derender API with generated-output-shaped JSON, and its fix surface is derender payload validation before decode. PR #45390 includes the GHSA-83mh-6mwq-3hg9 batch-message fanout fix class: it bounds the outer BatchChatCompletionRequest.messages conversation list to prevent one request from creating many conversation/request objects before normal generation. This report has no batch conversation list and does not rely on n; one derender request can instead supply oversized nested GenerateResponse choices and token IDs that are detokenized and returned directly. A batch-message max_length limit would not bound derender generate_response(s) or per-choice token/logprob structures. The completed local report titled "Explicit truncation_side disables tokenizer-level prompt truncation" is also distinct. That report used /v1/completions and /v1/chat/completions with ordinary prompt text plus truncate_prompt_tokens and explicit truncation_side; its root cause was the renderer omitting tokenizer-level max_length and the pre-tokenization character guard before post-token slicing; its sink was prompt tokenization; and its fix surface was preserving tokenizer-level truncation or rejecting over-budget prompts before tokenization. This derender report uses /v1 derender routes, has no prompt text tokenization or truncation-side control, starts from caller-supplied generated-output token IDs, and needs aggregate bounds on derender generate_response(s), choices, token IDs, logprobs, parser inputs, and response construction before detokenization. Other adjacent vLLM advisories for Rust/gRPC token-id and logprob bounds, structured-output grammar amplification, repetition-detection windows, and pooling/rerank batch fanout are distinct. Those issues affect Rust/gRPC request conversion, grammar compilation, scheduler loops, or engine fanout. This issue affects /v1 derender postprocessing of caller-supplied generated-output objects and requires derender-specific request validation before detokenization. Resources vllm/entrypoints/serve/render/api_router.py vllm/entrypoints/serve/disagg/protocol.py vllm/renderers/online_derenderer.py vllm/entrypoints/serve/render/serving.py vllm/entrypoints/openai/api_server.py PR #43606: https://github.com/vllm-project/vllm/pull/43606 PR #44285: https://github.com/vllm-project/vllm/pull/44285 GHSA-3mwp-wvh9-7528: https://github.com/vllm-project/vllm/security/advisories/GHSA-3mwp-wvh9-7528 PR #45390: https://github.com/vllm-project/vllm/pull/45390 Release v0.23.0: https://github.com/vllm-project/vllm/releases/tag/v0.23.0`

OWASP A06LLM04 · Model DoSOWASP LLM
Get guardrail →

Django REST framework: Potential bypass of Django `DATA_UPLOAD_MAX_MEMORY_SIZE` when parsing oversized JSON and urlencoded request bodies via DRF `request.data`

Summary While investigating Django REST Framework's request parsing behavior, I identified that DRF's high-level request.data parsing appears to bypass Django's configured DATA_UPLOAD_MAX_MEMORY_SIZE protection for application/json and application/x-www-form-urlencoded request bodies. In the tested configurations, Django correctly raises RequestDataTooBig when applications access request.body or Django's native request.POST, but DRF successfully parses the same oversized payloads through request.data. This behavior appears to occur because DRF passes the underlying HttpRequest object directly to parsers, which consume the request stream through Django's lower-level streaming interface rather than the guarded request.body path. I am reporting this privately because I am unsure whether this behavior is considered part of DRF's intended security boundary, but it appears to bypass a documented Django request-size protection for common DRF request parsing paths and may have availability implications. What I Verified I verified the behavior locally using the following combinations: Django 6.0.7 + DRF 3.17.1 → Affected Django 6.0.7 + DRF current upstream main → Affected For both versions, the observed behavior was: `` Django request.body → RequestDataTooBig Django request.POST (application/x-www-form-urlencoded) → RequestDataTooBig Django request.read() → Reads the entire oversized request body DRF request.data → Successfully parses oversized JSON and urlencoded request bodies ` I also confirmed that: multipart/form-data remains protected because DRF delegates multipart parsing to Django's multipart parser. The behavior reproduces on both direct WSGI and ASGI servers without a reverse proxy or external request-size middleware. Technical Details The relevant execution flow is: ` APIView ↓ rest_framework.request.Request ↓ request.data ↓ Request._load_data_and_files() ↓ Request._parse() ↓ Request._load_stream() ↓ self._stream = self._request ↓ JSONParser.parse(...) or FormParser.parse(...) ↓ stream.read() / json.load(...) ` The important implementation detail is that DRF assigns the original Django HttpRequest object as the parser stream. Unlike request.body and Django's native form parsing, consuming the stream through HttpRequest.read() does not trigger Django's RequestDataTooBig protection. As a result, DRF's built-in parsers successfully consume oversized request bodies that Django itself would reject through its higher-level request interfaces. Reproduction Steps Environment Python 3.13 Django 6.0.7 Django REST Framework 3.17.1 (also reproduced on current upstream main) Configure: `python DATA_UPLOAD_MAX_MEMORY_SIZE = 10 ` Create a simple DRF API view: `python from rest_framework.views import APIView from rest_framework.response import Response class DemoView(APIView): def post(self, request): return Response(request.data) ` Start the application. Send an oversized JSON request: ` POST /demo Content-Type: application/json Content-Length: >10 bytes ` Example: `json { "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..." } ` Observed: ` HTTP 200 JSON successfully parsed ` Now compare against: `python request.body ` Observed: ` RequestDataTooBig ` Likewise, compare against: `python request.POST ` using ` application/x-www-form-urlencoded ` Observed: ` RequestDataTooBig ` This demonstrates different enforcement depending on which request API is used. Root Cause Django documents HttpRequest.read() as a streaming interface. DRF exposes request.data as the primary high-level request parsing API. Currently, DRF forwards the raw Django request stream directly to parsers before any request-size validation equivalent to Django's request.body path occurs. Consequently: JSONParser FormParser fully consume oversized request bodies despite Django's configured request-size limit. Security Impact This does not appear to introduce: Authentication bypass Authorization bypass Remote code execution Information disclosure Integrity compromise However, it may reduce the effectiveness of deployments relying on Django's DATA_UPLOAD_MAX_MEMORY_SIZE to limit request-body resource consumption. Potential consequences include: Additional memory allocation during JSON parsing Additional CPU usage while decoding large JSON payloads Increased resource consumption when handling oversized request bodies Reduced effectiveness of Django's configured request-size protection for DRF endpoints using request.data The practical impact depends on deployment configuration, including: upstream request-size limits reverse proxy configuration authentication rate limiting endpoint exposure Memory Observations During local testing I observed successful parsing of oversized request bodies despite the configured limit. Representative measurements showed significantly increased memory allocation while parsing large JSON and urlencoded payloads. I intentionally did not perform destructive concurrency testing or attempt to exhaust system resources. Scope Confirmed affected: application/json application/x-www-form-urlencoded Confirmed not affected: multipart/form-data Suggested Fix Direction One possible approach would be for DRF to enforce Django's configured DATA_UPLOAD_MAX_MEMORY_SIZE before handing the raw request stream to parsers that fully materialize request bodies in memory. This would preserve Django's configured request-size protection for the common request.data` API without requiring broader changes to Django's documented streaming interface. Versions Tested Affected: Django 6.0.7 + DRF 3.17.1 Django 6.0.7 + DRF current upstream main I did not perform a complete historical version bisect. Disclosure I have not publicly disclosed this behavior. I am submitting it privately in accordance with the project's security policy because I am unsure whether maintainers consider this part of DRF's intended security boundary. Note: Thank you for taking the time to review this report. If you determine that this behavior should be addressed, I would be happy to help investigate further, develop a fix, add regression tests, and submit a patch if you'd find that helpful. I have experience as a Python/Django software engineer, security researcher, and open-source contributor, and I'd be glad to contribute if you think that would be useful.

OWASP A06LLM04 · Model DoSOWASP Web
Get guardrail →

Django REST framework: AdminRenderer may disclose GET-protected data when rendering invalid write requests

Summary AdminRenderer may disclose data that would normally be protected by GET permissions when rendering a 400 Bad Request response for an invalid write request. If a view allows POST (or another write method) but denies GET, an invalid request rendered through AdminRenderer can invoke the view's GET handler and include data from the GET representation in the generated HTML response. This behavior appears to be specific to AdminRenderer and does not affect the normal JSON rendering path. --- Details While investigating the AdminRenderer rendering flow, I observed that invalid write requests are rendered by temporarily overriding the request method and invoking the view's GET handler: `` with override_method(view, request, "GET") as request: response = view.get(request, view.args, *view.kwargs) data = response.data ` This execution path differs from a normal GET request. Under normal request processing, a GET request flows through: ` APIView.dispatch() └── APIView.initial() └── APIView.check_permissions() ` However, during AdminRenderer rendering, the renderer directly invokes: view.get(...) A view whose permission class explicitly allowed POST but denied GET still executed its GET handler while rendering an invalid POST request through AdminRenderer. As a result, data intended to be available only through an authorized GET request was included in the generated HTML response. --- Proof of Concept Using a standard ListCreateAPIView. Permission class: ` class ProbePermission(BasePermission): def has_permission(self, request, view): return request.method == "POST" View: class View(ListCreateAPIView): renderer_classes = (AdminRenderer, JSONRenderer) permission_classes = (ProbePermission,) serializer_class = ProbeSerializer def get_queryset(self): return [ { "name": "visible", "secret": "GET-ONLY-SECRET", } ] ` Expected Behaviour ` GET request → 403 Forbidden Invalid POST request → 400 Bad Request → Response should contain only validation errors. → GET-only data should not be rendered. ` Observed Behaviour ` GET request → 403 Forbidden Invalid POST request rendered through AdminRenderer → 400 Bad Request → HTML response contains: GET-ONLY-SECRET ` Tthe same behavior is shown using a minimal APIView implementation. Observed results: ` minimal.post_400.handler_calls = [ ("post", "POST"), ("get", "GET") ] minimal.post_400.contains_secret = True ` Generic view reproduction: ` generic.direct_get.status = 403 generic.direct_get.contains_secret = False generic.post_400.status = 400 generic.post_400.contains_secret = True generic.post_400.permission_calls = [ ("GenericAdminView", "POST"), ... ("GenericAdminView", "OPTIONS") ] generic.post_400.queryset_calls = [ ("GenericAdminView", "GET"), ... ] ` These observations indicate that direct GET requests are correctly denied, while the simulated GET used during AdminRenderer rendering can still retrieve the protected representation. --- Impact This issue may result in information disclosure when all of the following conditions are met: AdminRenderer is enabled. The client negotiates the HTML renderer (for example using Accept: text/html). The application permits POST (or another write method). GET requests are denied by the configured permission class. The invalid write request returns 400 Bad Request. The GET representation contains information that the requester would normally not be permitted to access. This issue does not appear to affect: JSON rendering Standard API responses Successful write requests The behavior appears limited to the HTML rendering path used by AdminRenderer. --- Suggested Fix Possible approaches include: Perform equivalent permission checks before executing the simulated GET request. Avoid invoking view.get() when the corresponding GET request would not be permitted. Fall back to rendering only serializer/form validation errors instead of retrieving the GET representation. A regression test could create a permission class that allows POST while denying GET, then verify that an invalid POST rendered with AdminRenderer does not include data from the protected GET representation. --- Environment Repository: encode/django-rest-framework Branch tested: security-audit-drf Commit tested: cf582fb58e9e5ffcc8ed78a2cb9aaa8f4865666a`

LLM02 · Insecure OutputOWASP Web
Get guardrail →

Echo: Encoded slash (%2F) bypasses route-level protection and exposes static files

Summary Echo's router and static file handler disagree on URL path decoding. The router matches routes using the raw encoded path (preserving %2F as-is), while StaticDirectoryHandler unescapes %2F to / before resolving filesystem paths. This allows an attacker to bypass route-level access controls and read static files without authorization. Details Root cause 1 — router.go lines 798-802: The router uses req.URL.RawPath for route matching when useEscapedPathForRouting is false (the default). This means /admin%2Fsecret.txt is treated as a single path segment and does NOT match the /admin/* route pattern. ``go if !r.useEscapedPathForRouting && req.URL.RawPath != "" { path = req.URL.RawPath } ` Root cause 2 — echo.go lines 559-568: StaticDirectoryHandler calls url.PathUnescape() on the path parameter before opening files. This converts %2F back to /, resolving admin/secret.txt on disk. `go if !disablePathUnescaping { tmpPath, err := url.PathUnescape(p) p = tmpPath } name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/"))) ` PoC (Screenshot) Sample: <img width="1291" height="970" alt="image" src="https://github.com/user-attachments/assets/0bc58059-3e6d-4678-ab25-a5c79b006738" /> 403: <img width="526" height="194" alt="image" src="https://github.com/user-attachments/assets/2f55ffdd-87b2-4a1b-8a13-130ebad0f257" /> Bypass with encoded slash: <img width="592" height="203" alt="image" src="https://github.com/user-attachments/assets/1191cd39-ae8f-4d7e-8fb1-cb9cf31f484f" /> Impact Unauthorized static file disclosure. Applications that protect route prefixes with authentication middleware while also serving static files from a broader root are vulnerable. An attacker only needs to encode the slash (/ → %2F) in the URL to bypass all route-level protection. Common affected pattern: `go adminGroup := e.Group("/admin", authMiddleware) e.StaticFS("/", os.DirFS("public")) ``

OWASP A01OWASP Web
Get guardrail →

vLLM: Completion prompt lists fan out into unbounded engine requests

Summary The /v1/completions request model accepts prompt as a list of text prompts or a list of token-id prompts without any outer prompt-count bound. The serving path turns each element into a separate engine input, creates one engine generator per element, merges all generators, and allocates a response slot per prompt. An authenticated API client can therefore turn one request into an attacker-chosen number of backend subrequests before any aggregate request-count budget is enforced. Technical Details CompletionRequest.prompt allows both list-shaped prompt inputs and scalar prompts: ``python vllm/entrypoints/openai/completion/protocol.py prompt: ( list[Annotated[int, Field(ge=0)]] | list[list[Annotated[int, Field(ge=0)]]] | str | list[str] | None ) = None ` The validator only requires some prompt-like input to be present: `python def validate_prompt_and_prompt_embeds(cls, data): prompt = data.get("prompt") prompt_embeds = data.get("prompt_embeds") ... if prompt_is_empty and embeds_is_empty: raise VLLMValidationError(...) ` The renderer then expands list-shaped prompts as a sequence. prompt_to_seq() wraps a scalar string or a single token-id list, but returns a list[str] or list[list[int]] unchanged: `python vllm/renderers/inputs/preprocess.py def prompt_to_seq(prompt_or_prompts): if isinstance(prompt_or_prompts, (dict, str, bytes)) or ( len(prompt_or_prompts) > 0 and is_list_of(prompt_or_prompts, int) ): return [prompt_or_prompts] return prompt_or_prompts ` OnlineRenderer.preprocess_completion() appends that whole sequence, and the renderer processes every element: `python vllm/renderers/online_renderer.py prompts = list[SingletonPrompt | bytes]() if prompt_input is not None: prompts.extend(prompt_to_seq(prompt_input)) ... parsed_prompts = [ prompt if isinstance(prompt, bytes) else parse_model_prompt(model_config, prompt) for prompt in prompts ] return await renderer.render_cmpl_async(parsed_prompts, tok_params, ...) ` Finally, completion serving creates one backend generator and one response slot per rendered prompt: `python vllm/entrypoints/openai/completion/serving.py generators: list[AsyncGenerator[RequestOutput, None]] = [] for i, engine_input in enumerate(engine_inputs): ... generator = self.engine_client.generate(...) generators.append(generator) result_generator = merge_async_iterators(generators) num_prompts = len(engine_inputs) ... final_res_batch: list[RequestOutput | None] = [None] num_prompts ` The violated invariant is that one HTTP request should have a bounded backend request count. Current code enforces per-prompt token and sampling limits, but not the number of prompts in the outer completion request. PoV A minimal oversized request keeps normal generation parameters small but supplies a large outer prompt list: `json { "model": "served-model", "prompt": ["x", "x", "x"], "max_tokens": 1, "n": 1 } ` Scaling the prompt array to tens or hundreds of thousands of short entries makes the server allocate, preprocess, schedule, merge, and buffer one subrequest per entry. The same applies to token-id prompt lists: `json { "model": "served-model", "prompt": [[1], [1], [1]], "max_tokens": 1, "n": 1 } ` The intended negative control is a scalar prompt: `json { "model": "served-model", "prompt": "x", "max_tokens": 1, "n": 1 } ` The scalar string is wrapped as one prompt; the list form is not bounded and fans out by list length. Impact An authenticated API client can make one /v1/completions request consume CPU, memory, async task scheduling, engine request slots, and response buffering proportional to an attacker-chosen outer prompt list. This can starve or disrupt other tenants sharing the same vLLM server. The report does not claim unauthenticated access, confidentiality impact, integrity impact, code execution, or impact where /v1/completions is not reachable by untrusted or semi-trusted clients. Suggested Fix Reject oversized prompt lists before renderer preprocessing. Add an outer prompt-count limit to CompletionRequest.prompt when the prompt is list[str] or list[list[int]], and consider making the limit configurable in the same style as the batch-chat and sampling-list bounds. The check should run before OnlineRenderer.preprocess_completion() expands the prompt sequence, so oversized requests do not allocate parsed prompt lists, async render/tokenization tasks, engine generators, or response result slots. Regression coverage should include a scalar prompt, a bounded prompt list, an oversized list[str], and an oversized list[list[int]]. The oversized requests should fail with a controlled validation error before any backend generator is created. Affected Package/Versions Package ecosystem: pip Package name: vllm Affected range confirmed by source proof: >=0.19.0, <=0.24.0; current main at cbe9c40f998f13975b967773ac7e7920e115387f remains affected. Patched versions: unknown. Latest release checked: v0.24.0, published on 2026-06-29. GitHub Advisory Metadata Package ecosystem: pip Package name: vllm Vulnerable version range: >=0.19.0, <=0.24.0 Patched versions: unknown Advisory History Public issue and PR searches for CompletionRequest prompt list, "prompt" "list[str]" "completion", and "CompletionRequest" "max_length" did not find an existing report or fix for this exact path. The closest published advisory is GHSA-3mwp-wvh9-7528, "OOM Denial of Service via Unbounded n Parameter in OpenAI API Server", patched in 0.19.0. This report is distinct because it keeps n=1 and uses the /v1/completions prompt outer list to create one engine request per prompt. The fix invariant is an outer prompt-count and aggregate request budget, not only a generated-sequence-count cap. The closest public PR is vllm-project/vllm#45390, which covers multiple DoS fixes including GHSA-83mh-6mwq-3hg9 for BatchChatCompletionRequest.messages. That PR adds an outer bound to batch chat conversations, but its diff does not touch vllm/entrypoints/openai/completion/protocol.py or vllm/entrypoints/openai/completion/serving.py. Prior local/private report families checked included pooling/rerank batch fanout, derender token-id postprocessing, explicit truncation_side` tokenizer-limit bypass, Python disaggregated generate prompt-length bypass, and priority scheduling. Those reports differ by endpoint, attacker-controlled field, sink, and fix surface.

OWASP A06LLM04 · Model DoSOWASP LLM
Get guardrail →

Pydantic AI: Unvalidated UploadedFile references in UI adapters allow server-side file access using the application's credentials

Summary A client that can submit message history to a Pydantic AI UI adapter can reference arbitrary files in the application's model-provider or cloud-storage account. The server forwards the reference to the model provider, which fetches it using the server's own credentials, allowing the client to read files it should not have access to. Details UI adapters reconstruct file parts from client-submitted message history and forward them to the model provider. File URL parts are validated against a scheme allowlist before being forwarded, but UploadedFile references — which point to a file by provider file ID or cloud-storage URI (e.g. s3://…, gs://…) — were forwarded without validation. Because the provider resolves an UploadedFile using the server-side identity (IAM role, service account, or provider API key) rather than the client's, a client that crafts message history containing an attacker-chosen UploadedFile can cause the server to read objects belonging to its own account or to other tenants, given a referenceable identifier. Impact Applications that pass untrusted client-submitted message history to an agent through a UI adapter (such as the Vercel AI adapter). Exploitation requires the attacker to reference a valid file identifier; depending on how the application names objects, such identifiers are not always unguessable. Patches Upgrade to 1.106.0 (1.x) or 2.0.0b6 (the 2.x beta line), which validate UploadedFile references on client-submitted messages the same way file URLs are validated. Workarounds If users cannot upgrade, do not pass untrusted client-submitted message history to the agent, or strip UploadedFile parts from incoming messages before running the agent.

OWASP A10LLM02 · Insecure OutputOWASP LLM
Get guardrail →

Nuxt dev server discloses project root and workspace UUID via the Chrome DevTools workspace endpoint

Impact When a Nuxt dev server is bound to a network-reachable interface (for example nuxt dev --host for on-device testing), the default-enabled Chrome DevTools workspace endpoint GET /.well-known/appspecific/com.chrome.devtools.json returns the absolute project root (workspace.root, i.e. rootDir) and a persistent per-project workspace UUID. GHSA-rq7w-g337-39qq added a gate (isLocalDevRequest) intended to restrict this endpoint to local requests, but that gate is header-based: it trusts request metadata rather than the connected peer address. A request with no Sec-Fetch-Site, Origin, and Referer headers (normal for a non-browser client such as curl) is treated as local, and the Host allow-list is compared against the attacker-supplied Host header. As a result, any unauthenticated host that can reach the dev server on the LAN can retrieve the project's absolute filesystem path and workspace UUID, for example with curl -H 'Host: localhost' http://<dev-host-lan-ip>:3000/.well-known/appspecific/com.chrome.devtools.json. This is information disclosure only: there is no file read, file write, or code execution reachable from the endpoint. It requires the dev server to be reachable beyond loopback and experimental.chromeDevtoolsProjectSettings to be enabled (it defaults to true). Production builds are unaffected, because the endpoint is registered only as a development handler. Patches Fixed in nuxt@4.5.1 and nuxt@3.21.10. The endpoint now additionally requires the connected TCP peer to be a loopback address, verified from the socket rather than from request headers, so a non-loopback LAN client is rejected regardless of the Host, Origin, Referer, or Sec-Fetch-* headers it sends. The shared header-based check is left unchanged, so the CSRF / same-origin behaviour that other dev handlers rely on is preserved. After this fix, Chrome DevTools workspace auto-mapping only works when the browser reaches the dev server over loopback (localhost / 127.0.0.1 / ::1), which matches the feature's intent (the browser and dev server sharing a filesystem). Workarounds Do not bind the dev server to a non-loopback interface on an untrusted network, or restrict access to the dev port with a firewall. Disable the feature by setting experimental.chromeDevtoolsProjectSettings: false in nuxt.config.

LLM02 · Insecure OutputOWASP Web
Get guardrail →

Nuxt: Unauthenticated CPU exhaustion parsing and hashing the Nuxt island endpoint body before hash validation

Impact The internal island renderer endpoint (/__nuxt_island/...) decodes and hashes attacker-controlled request input before it validates the URL-resident hash. An unauthenticated POST /__nuxt_island/<name>_<anything>.json with a large JSON body (for example ~4.6 MB / 150k keys) is fully read, destr-parsed, and run through ohash before the request is rejected with a 400. Because Nitro runs on a single event loop, this both wastes CPU on the doomed request and delays every concurrent request. A low request rate is enough to degrade or stall the server. No valid hash and no authentication are required. Patches Fixed in nuxt@4.5.1 and nuxt@3.21.10. The island handler now enforces a raw body-size cap (413) and a JSON nesting-depth cap (400) before parsing or hashing, so oversized or deeply nested input is rejected cheaply. Workarounds Put a small request-body limit in front of /__nuxt_island/ at your reverse proxy / edge (islands legitimately send only a compact props payload), or disable server components if unused.

Nuxt: Server-Side Remote Code Execution via Runtime Template Injection in Nuxt Server Island Props

Impact Nuxt server islands accept props via the /__nuxt_island/ endpoint. When vue.runtimeCompiler: true is enabled (off by default) and the application has a server island component that forwards props into Vue's dynamic component resolution (<component :is>, resolveDynamicComponent, or h()), an attacker can inject a template key into the island props to achieve server-side remote code execution in the Nitro process. ``json { "as": { "template": "<attacker-controlled>" } } ` Vue's runtime template compiler compiles and executes the attacker-controlled template in the server process. The same primitive also works on the client side when the runtime compiler is active there, though the server-side path is the primary concern. Some component libraries expose a polymorphic as / asChild prop that forwards its value into <component :is>; @nuxt/ui (via reka-ui) is a widely used example. An application is affected if such a component receives the attacker-controlled value, provided vue.runtimeCompiler is also enabled. Note this does not require the island author to explicitly forward a prop: island props that the island component does not declare fall through as attributes onto its single root element (standard Vue attribute inheritance), so an island whose root is a polymorphic component receives the attacker's as value implicitly. These libraries are not themselves vulnerable; they are noted only because they commonly provide the dynamic-component sink. Common configurations that satisfy the preconditions The flaw is in Nuxt core. The library below is not itself vulnerable; it is noted because it commonly provides the dynamic-component sink an application might inadvertently expose. @nuxt/ui (via its underlying reka-ui primitives) exposes a polymorphic as / asChild prop that is forwarded into Vue's dynamic-component resolution. Installing @nuxt/ui does not by itself register any component as a server island. Exploitation requires the application to define an island component (a .server.vue file) whose rendered output puts the attacker-controlled value on such a component's as / asChild prop; with vue.runtimeCompiler: true, the attacker-controlled template is then compiled and executed. Because undeclared island props fall through as attributes to the single root, this can happen without any explicit binding: an island whose root is a reka-ui / @nuxt/ui component is enough. An example vulnerable island (the as value falls through to UButton, no explicit forwarding needed): `vue <!-- components/MyWidget.server.vue --> <template> <UButton>Save</UButton> </template> ` The island URL hash (/__nuxt_island/<Name>_<hash>.json) is a deterministic (unsalted) content hash, not an authentication token. It provides integrity relative to the URL but is not a security boundary: an attacker who knows the component name and desired props can compute a valid hash. Mitigating factors vue.runtimeCompiler is off by default in Nuxt. The vast majority of Nuxt applications are not affected. Exploitation requires a second precondition: the application must have a server island component that puts an attacker-controlled value onto a dynamic-component path (<component :is>, resolveDynamicComponent, h(), or a polymorphic as / asChild prop). This can occur explicitly or via attribute fallthrough when the island's root is such a component. SSG / static deployments are largely unreachable via this vector (no server process to exploit). Island component names are constrained to the build-time component registry; an attacker cannot resolve arbitrary components. Affected versions Nuxt >=3.4.0 <3.21.10 and >=4.0.0 <4.5.1, and only when vue.runtimeCompiler: true and component islands are active. Earlier versions did not allow the Vue compiler to be enabled in the server bundle (the compiler dependencies have been mock-aliased on the server since v3.0.0-rc.1), so the runtime-compilation path is not reachable. Nuxt 2 is not affected (no server islands). Patches When vue.runtimeCompiler is enabled, island requests whose decoded props contain a template key at any depth are rejected with an HTTP 400 and a diagnostic suggesting the author rename the prop or disable the runtime compiler. The guard is gated on the runtime compiler being enabled, so the default configuration (compiler off) is unaffected and legitimate props that merely contain a template field (for example CMS content) continue to render. A render key is not rejected: island props arrive as JSON, so a render value can only be an inert string, which Vue ignores. Fixed in nuxt@4.5.1 and backported to nuxt@3.21.10. Workarounds Upgrade to nuxt@4.5.1 or nuxt@3.21.10. If you cannot immediately upgrade, you can mitigate by: 1. Ensure vue.runtimeCompiler is set to false (the default). 2. Do not forward island props into <component :is>, resolveDynamicComponent, or h() without sanitization. 3. As a defense-in-depth measure, deploy a WAF rule on /__nuxt_island/ that URL-decodes and JSON-parses the props value and blocks any object property named template or render` in the decoded island props, inspecting both query and body for all methods. Note: this only covers direct and browser-originated island requests; initial-SSR internal island renders do not transit the edge and a WAF alone does not close the vector.

OWASP A03LLM01 · Prompt InjectionOWASP Web
Get guardrail →

Nuxt: Unauthorized Component Instantiation via Server Island Props

Impact Nuxt server islands accept props via the /__nuxt_island/ endpoint. When an application has a server island component that forwards props directly into Vue's dynamic component resolution (<component :is>, resolveDynamicComponent, or h()), an attacker can pass a plain string value (rather than a component definition) to instantiate any globally-registered Vue component or any native HTML element. For example: ``json { "as": "SomeGlobalComponent" } ` ...resolves and renders SomeGlobalComponent if it is globally registered, even though the attacker should only be able to drive props for the island's declared component. Similarly, { "as": "iframe" } renders an <iframe> element. Unlike the primary RCE vector (GHSA-9473-5f9j-94wq), this does not require vue.runtimeCompiler to be enabled. A plain string prop is sufficient to trigger component resolution. The template/render key guard that addresses the RCE vector does not block plain string values. Some component libraries expose a polymorphic as / asChild prop that forwards its value into <component :is>; @nuxt/ui (via reka-ui) is a widely used example. An application is affected if such a component receives the attacker-controlled value inside a server island. Note this does not require explicit prop forwarding: island props the island component does not declare fall through as attributes onto its single root element, so an island whose root is a reka-ui / @nuxt/ui component receives the attacker's as value implicitly. Unlike the RCE vector, no vue.runtimeCompiler is required, which makes this vector reachable in more configurations. These libraries are not themselves vulnerable; they are noted only because they commonly provide the dynamic-component sink. Installing @nuxt/ui does not by itself register any component as a server island: the application must define the island (a .server.vue file). Mitigating factors Exploitation requires a server island component that puts an attacker-controlled value onto a dynamic-component path (<component :is>, resolveDynamicComponent, h(), or a polymorphic as / asChild prop), either explicitly or via attribute fallthrough when the island's root is such a component. Reachable components are limited to what is actually in the island app's global registry. In a default pages-enabled app that is RouterView and RouterLink (both registered globally by the pages router plugin, which runs even in component islands), plus any components/global/ component and any component a module registers globally. RouterLink in particular renders an attacker-influenced <a> (and, because an island that declares no props forwards all props, to and other RouterLink props ride the same fallthrough). Vue built-ins (Transition, KeepAlive, Teleport, Suspense) and Nuxt auto-imports (ClientOnly, NuxtLink, NuxtPage, etc.) are NOT in the island app's global registry and cannot be resolved this way; an unresolved name instead renders as a native HTML element (the element-injection half of this issue). Declaring the props an island accepts, or setting inheritAttrs: false on it, prevents an undeclared as from falling through to a polymorphic root and neutralizes this vector. Arbitrary JavaScript execution is not possible through this vector (no template/render compilation). Island component names are constrained to the build-time component registry; an attacker cannot resolve arbitrary components. Affected versions Nuxt >=3.1.0 <3.21.10 and >=4.0.0 <4.5.1, with component islands active. The island prop-forwarding behavior has existed since server islands were introduced in v3.1.0, and this vector does not depend on vue.runtimeCompiler. Nuxt 2 is not affected. Note the patch (below) closes the implicit attribute-fallthrough path, which is the majority case. An island that explicitly forwards an untrusted prop into dynamic component resolution (or forwards it under a prop name other than as) remains the application's responsibility in every version; see Workarounds. Patches Fixed in nuxt@4.5.1 and nuxt@3.21.10. Patched releases reject a top-level as island prop (HTTP 400 at the /__nuxt_island/ endpoint). This closes the implicit path: island props an island does not declare fall through as attributes onto its single root, so a top-level as would otherwise reach a polymorphic root component's as prop (the reka-ui / @nuxt/ui convention) and drive dynamic component resolution without the author binding it. Rejecting the top-level as prop blocks that fallthrough while leaving nested data and other prop names untouched. The framework deliberately does not attempt to block every case: it cannot safely tell a string used as data from one used as a component selector, and it has no island-local hook into Vue's h() or resolveDynamicComponent(). An island that explicitly forwards an untrusted value into <component :is> / h() / resolveDynamicComponent(), or that forwards it under a different polymorphic prop name, is therefore not covered by the patch and must follow the guidance below. The Nuxt documentation now warns against this. Workarounds Upgrade to nuxt@4.5.1 or nuxt@3.21.10. That upgrade also removes the related object-prop RCE (GHSA-9473-5f9j-94wq). In addition, in any version: 1. Do not forward island props into <component :is>, resolveDynamicComponent, or h(). Map an untrusted discriminator through a closed allowlist of imported component definitions instead of passing the raw prop value. 2. Declare the props an island accepts, or set inheritAttrs: false` on it, so request input cannot fall through to a polymorphic root component. 3. Avoid registering sensitive components globally that could leak information if instantiated by an attacker.

OWASP A03LLM01 · Prompt InjectionOWASP Web
Get guardrail →

Nuxt runtime payload cache discloses another user's SSR data across users and to unauthenticated clients

Impact When a page is covered by routeRules cache / swr / isr, Nuxt enables runtime payload extraction and serves /<page>/_payload.json. On affected versions the renderer stored the SSR payload in the shared cache:nuxt:payload storage under a path-only key (no cookie, authorization, or cache.varies dimension) and, on a later payload request, returned the cached entry before route middleware / page guards ran again. As a result, once any authenticated user warms a protected, cached page, a subsequent GET /<page>/_payload.json from an unauthenticated client or a different authenticated user receives the first user's payload: the full SSR data for that route, including anything loaded via useFetch / useAsyncData (for example /api/me: profile, tenant, billing, token-like values). The HTML response stays correctly varied and protected; only the extracted payload leaks. Both cross-user (A warms, B receives A) and unauthenticated disclosure are exploitable. cache.varies does not mitigate it, because the payload cache ignores varies. Introduced when runtime payload extraction landed for cached routes (#34410); the regression is specific to the 4.x line, where the runtime cache:nuxt:payload storage was added and the import.meta.prerender gate on the payload-cache read/writes was dropped. The 3.x line shipped the same feature with the gate intact and is not affected. Patches Fixed in nuxt@4.5.1. Runtime payload-cache reads and writes are again confined to prerendering (import.meta.prerender); at runtime, /<page>/_payload.json follows the normal render path so route middleware, routeRules.appMiddleware, and page guards run for the current request. main / v5 and the 3.x line already had this property, so 3.x is not affected. Workarounds Set experimental.payloadExtraction: false (reporter-validated): the standalone /_payload.json endpoint returns 404 and the page still serves a 200 with an inline payload. Do not apply cache / swr / isr to authenticated pages that render user-specific SSR data. As defense-in-depth, require authentication for /**/_payload.json at a proxy / CDN. After upgrading, purge any CDN / platform cache that may already hold protected payloads.

Nuxt route rules silently dropped for mixed-case paths, bypassing appMiddleware auth gates (incomplete fix for CVE-2026-53721)

Impact Nuxt matches route rules case-insensitively by default (mirroring vue-router's default sensitive: false routing). The fix for GHSA-mm7m-92g8-7m47 / CVE-2026-53721 lowercased the lookup path before matching route rules, but the route-rule keys compiled into the matcher were left verbatim. As a result, any route rule whose key contains an uppercase character (for example /Admin, /Dashboard/**, or the rules Nuxt derives from PascalCase/camelCase page files such as pages/Admin.vue) never matches, because every lookup is folded to lowercase while the key stays mixed-case. vue-router still serves the page case-insensitively, so the page renders with none of its Nuxt route-rule protections applied. The most serious consequence is an authorization bypass: an appMiddleware rule used as an auth gate (routeRules: { '/Admin/dashboard': { appMiddleware: 'auth' } }) is dropped, and /Admin/dashboard, /admin/dashboard, and /ADMIN/dashboard all render the protected page (and its SSR-fetched data) to an unauthenticated visitor instead of redirecting to login. The same gap drops Nuxt's other app-side route-rule behaviours for mixed-case keys, including the client redirect middleware, the app-side ssr: false decision, prerender, and payload handling. Patches Fixed in nuxt@4.5.1 (4.x) and nuxt@3.21.10 (3.x). The route-rule matcher now case-folds the compiled keys the same way it folds the lookup path, so key and lookup normalisation are symmetric. Both sides are gated on router.options.sensitive: with sensitive: true (case-sensitive routing) configured casing is preserved on both sides. Scope note: server-emitted per-route headers, server redirect, and proxy are matched by Nitro's own case-sensitive route-rule matcher, not by Nuxt's app-level matcher. They are unchanged by this advisory. The fix covers the app-level protections Nuxt owns (appMiddleware, appLayout, the client redirect middleware, the app ssr decision, prerender, and payload). Workarounds If you cannot upgrade immediately, any one of: Key all routeRules (and name your page files) in lowercase, so the keys already match the folded lookup path. Set router: { options: { sensitive: true } } so routing and route-rule matching are both case-sensitive and exact (requests must then use the exact casing). Enforce the sensitive protections server-side independently of route rules (for example a server middleware that checks auth), which does not rely on case-insensitive route-rule matching.

OWASP A01OWASP Web
Get guardrail →

Showing 1–20 of 521 threats