vLLM: Completion prompt lists fan out into unbounded engine requests
Medium6.5CVE-2026-73559 · Published Aug 13, 2026 · updated Sep 10, 2026
## 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...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| vllm PyPI | >= 0.19.0, < 0.26.0 | 0.26.0 |
Details and references
## 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 e
- CVSS 3.1
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
- Severity from
- GitHub (reviewed advisory)
- Weakness
- CWE-400
- Also known as
- CVE-2026-73559, PYSEC-2026-3704
More vLLM advisories
All vLLM| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Sep 8 | vLLM: Cross-User Data Leak Vulnerability | Medium5.3 | 0.27.0 |
| Sep 4 | vLLM: Incomplete CVE-2025-62164 remediation can be bypassed by concurrent prompt parts | Medium | 0.26.0 |
| Sep 4 | vLLM: denial of service | Medium5.3 | 0.26.0 |
| Sep 4 | vLLM: Unauthenticated Internal Path and Username Disclosure via Validation Error Messages | Medium5.3 | 0.26.0 |
| Sep 4 | vLLM: Derender endpoints decode caller-supplied GenerateResponse token IDs without output bounds | Medium4.3 | 0.26.0 |
| Jul 20 | vLLM denial of service via prompt embeds on M-RoPE models | High | 0.24.0 |