CVE-2026-71486: vLLM Derender Endpoints Decode Caller-supplied GenerateResponse Vulnerability
MODERATECVE-2026-71486 is a MODERATE (CVSS 4.3) derender endpoints decode caller-supplied generateresponse vulnerability (CWE-400: Uncontrolled Resource Consumption) affecting vLLM libraries in the PyPI ecosystem. The issue involves derender endpoints decode caller-supplied generateresponse token ids without output bounds. Remediation is available by upgrading affected packages to version 0.26.0.
CVE-2026-71486 at a Glance
Security Impact
CVSS: 4.3
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L).
How to Fix CVE-2026-71486
To remediate this vulnerability, upgrade all affected vLLM dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install vllm>=0.26.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show vllm
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-71486, inspect your installed packages using pip:
pip show vllm
Analyze the terminal output to locate any version of vLLM in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.26.0), your application is affected.
Note: The pip show vllm command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-400: Uncontrolled Resource Consumption, where the product does not properly control the allocation and maintenance of a limited resource. In vLLM, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-71486 Affected vLLM Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
vllm
|
PyPI |
< 0.26.0
|
0.26.0
|
References
Frequently Asked Questions
What is CVE-2026-71486?
Which vLLM versions are affected?
What version fixes CVE-2026-71486?
Is vllm affected?
What is the CVSS score of CVE-2026-71486?
What CWE is associated with CVE-2026-71486?
How can I check PyPI dependencies for CVE-2026-71486?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-8737-qx52-hjff",
"modified" : "2026-09-04T21:32:08Z",
"published" : "2026-09-04T21:32:07Z",
"aliases" : [ "CVE-2026-71486" ],
"summary" : "vLLM: Derender endpoints decode caller-supplied GenerateResponse token IDs without output bounds",
"details" : "## Summary\n\nThe `/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.\n\n## Technical Details\n\nThe 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.\n\nThe 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`.\n\nThe 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.\n\nNormal 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.\n\n## PoV\n\nThe 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.\n\n```python\n#!/usr/bin/env python3\nimport subprocess\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nSOURCE = Path(\".\")\n\ndef require_source_fact(path: str, needles: list[str]) -> None:\n text = (SOURCE / path).read_text()\n missing = [needle for needle in needles if needle not in text]\n if missing:\n raise AssertionError(f\"{path} missing expected facts: {missing}\")\n\ndef source_head() -> str:\n return subprocess.check_output([\"git\", \"rev-parse\", \"HEAD\"], cwd=SOURCE, text=True).strip()\n\n@dataclass\nclass Choice:\n index: int\n token_ids: list[int]\n\n@dataclass\nclass GenerateResponse:\n request_id: str\n choices: list[Choice]\n\nclass CountingTokenizer:\n def __init__(self) -> None:\n self.decode_calls = 0\n self.decoded_ids = 0\n def decode(self, token_ids: list[int], *, skip_special_tokens: bool = True) -> str:\n self.decode_calls += 1\n self.decoded_ids += len(token_ids)\n return \"x\" * len(token_ids)\n\ndef derender_completion_like_current_head(generate_responses: list[GenerateResponse], tokenizer: CountingTokenizer) -> tuple[int, int, int]:\n output_chars = 0\n choices = 0\n total_completion_tokens = 0\n for gen in generate_responses:\n for choice in gen.choices:\n if not choice.token_ids:\n raise ValueError(\"choice has empty or null token_ids\")\n decoded_text = tokenizer.decode(choice.token_ids, skip_special_tokens=True)\n output_chars += len(decoded_text)\n total_completion_tokens += len(choice.token_ids)\n choices += 1\n return choices, total_completion_tokens, output_chars\n\ndef make_payload(responses: int, choices_per_response: int, tokens_per_choice: int) -> list[GenerateResponse]:\n token_ids = [42] * tokens_per_choice\n 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)]\n\ndef run_case(name: str, payload: list[GenerateResponse]) -> None:\n tokenizer = CountingTokenizer()\n choices, completion_tokens, output_chars = derender_completion_like_current_head(payload, tokenizer)\n 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}\")\n\nrequire_source_fact(\"vllm/entrypoints/serve/render/api_router.py\", ['\"/v1/completions/derender\"', '\"/v1/chat/completions/derender\"', \"app.include_router(router)\"])\nrequire_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]\"])\nrequire_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)\"])\nprint(\"source_checks=ok\")\nprint(f\"source_head={source_head()}\")\nrun_case(\"negative_control\", make_payload(responses=1, choices_per_response=1, tokens_per_choice=32))\nrun_case(\"amplified_payload\", make_payload(responses=16, choices_per_response=4, tokens_per_choice=8192))\nprint(\"observation=derender decodes every caller-supplied token id before any max_model_len, max_tokens, max_num_seqs, or response-size check\")\n```\n\n\n## Impact\n\nAn 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.\n\nLikely 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`.\n\n## Suggested Fix\n\nValidate 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.\n\nAdd 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.\n\n## Affected Package/Versions\n\nConfirmed 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`.\n\nLatest 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.\n\n## Advisory History\n\nPR `#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.\n\nPublic 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.\n\nRelated public request-fanout and resource-bound advisories are distinct:\n\n- `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.\n- 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.\n\nThe 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.\n\nOther 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.\n\n## Resources\n\n- `vllm/entrypoints/serve/render/api_router.py`\n- `vllm/entrypoints/serve/disagg/protocol.py`\n- `vllm/renderers/online_derenderer.py`\n- `vllm/entrypoints/serve/render/serving.py`\n- `vllm/entrypoints/openai/api_server.py`\n- PR `#43606`: `https://github.com/vllm-project/vllm/pull/43606`\n- PR `#44285`: `https://github.com/vllm-project/vllm/pull/44285`\n- `GHSA-3mwp-wvh9-7528`: `https://github.com/vllm-project/vllm/security/advisories/GHSA-3mwp-wvh9-7528`\n- PR `#45390`: `https://github.com/vllm-project/vllm/pull/45390`\n- Release `v0.23.0`: `https://github.com/vllm-project/vllm/releases/tag/v0.23.0`",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "vllm"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "0.26.0"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/vllm-project/vllm/security/advisories/GHSA-8737-qx52-hjff"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-71486"
}, {
"type" : "WEB",
"url" : "https://github.com/vllm-project/vllm/pull/47260"
}, {
"type" : "WEB",
"url" : "https://github.com/vllm-project/vllm/commit/8e61b646e2d157f9b93451fa048f9c8530c8a67b"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/vllm-project/vllm"
}, {
"type" : "WEB",
"url" : "https://github.com/vllm-project/vllm/releases/tag/v0.26.0"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-400", "CWE-770" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-09-04T21:32:07Z",
"nvd_published_at" : "2026-08-17T20:16:45Z"
}
}