Skip to content
vLLMGHSA-8737-qx52-hjff

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

Medium4.3CVE-2026-71486 · Published Sep 4, 2026 · updated Sep 10, 2026

GitHub advisory

Affected versions

PackageAffectedFixed in
vllm
PyPI
< 0.26.00.26.0
Details and references

## 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: reques

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
Severity from
GitHub (reviewed advisory)
Weakness
CWE-400, CWE-770
Also known as
CVE-2026-71486, PYSEC-2026-3936

More vLLM advisories

All vLLM

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.