{"id":"CVE-2026-54236","aliases":["GHSA-hgg8-fqqc-vfmw","PYSEC-2026-3408"],"title":"vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router","summary":"vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router","severity":"medium","cvss":5.3,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","vendor":"vllm","product":"vllm","ecosystem":"pip","affected":["vllm < 0.24.0"],"patched":["vllm 0.24.0"],"published":"2026-06-17","updated":"2026-09-10","sourceUpdated":"2026-09-10T03:50:49.787104004Z","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-hgg8-fqqc-vfmw","references":[{"url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-hgg8-fqqc-vfmw"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-54236"},{"url":"https://github.com/vllm-project/vllm/pull/45119"},{"url":"https://github.com/vllm-project/vllm/commit/94923629729381d7f7c9efde72071a2441f7fd82"},{"url":"https://github.com/advisories/GHSA-hgg8-fqqc-vfmw"},{"url":"https://github.com/pypa/advisory-database/tree/main/vulns/vllm/PYSEC-2026-3408.yaml"},{"url":"https://github.com/vllm-project/vllm"},{"url":"https://pypi.org/project/vllm"}],"tags":["osv","pip","exploit-available","ghsa"],"epss":0.00927,"epssPercentile":0.58991,"exploits":{"nuclei":["CVE-2026-54236"],"checkedAt":"2026-09-23T07:14:17.082Z"},"exploitAvailable":true,"cwe":["CWE-532"],"ingestedAt":"2026-06-29T14:31:47.438Z","slug":"CVE-2026-54236","body":"## Overview\n\n# vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via the Anthropic API router\n\n**Researcher:** Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research\n**Severity:** CVSS 3.1 5.3 (Medium)  `AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N`\n**Target:** https://github.com/vllm-project/vllm\n\n---\n\n## Summary\n\nThe fix for CVE-2026-22778 / GHSA-4r2x-xpjr-7cvv (PRs #31987 and #32319) introduced `sanitize_message` and applied it at four FastAPI exception-handling sites in the OpenAI router. The sanitizer strips object-repr memory addresses (`<_io.BytesIO object at 0x7a95e299e750>` → `<_io.BytesIO object>`) before error messages reach the client, defeating the ASLR-bypass primitive that CVE-2026-22778 chained with a libopenjp2 heap overflow for RCE.\n\nThe fix is incomplete: response paths added to vLLM at or after the same time as the fix continue to echo `str(exc)` directly to clients without `sanitize_message`. The original Stage 1 primitive — sending malformed image bytes so PIL raises `UnidentifiedImageError` whose message contains the BytesIO object repr — reaches all of them unmodified and leaks the heap address verbatim in the response body.\n\nAll five lines below are present in `main` HEAD (`771e1e48b`, 2026-05-26).\n\n## Affected sites\n\nCurrent `main` HEAD (`771e1e48b`, 2026-05-26):\n\n| # | File | Line | Code |\n|---|---|---|---|\n| 1 | `vllm/entrypoints/anthropic/api_router.py` | 78 | `message=str(e),` (inside `POST /v1/messages` exception handler) |\n| 2 | `vllm/entrypoints/anthropic/api_router.py` | 124 | `message=str(e),` (inside `POST /v1/messages/count_tokens`) |\n| 3 | `vllm/entrypoints/anthropic/serving.py` | 808 | `error=AnthropicError(type=\"internal_error\", message=str(e)),` (SSE streaming converter) |\n| 4 | `vllm/entrypoints/speech_to_text/realtime/connection.py` | 75 | `await self.send_error(str(e), \"processing_error\")` (WebSocket event loop) |\n| 5 | `vllm/entrypoints/speech_to_text/realtime/connection.py` | 265 | `await self.send_error(str(e), \"processing_error\")` (WebSocket generation loop) |\n\n## Why the global exception handler does not save these paths\n\n`api_server.py` registers a catch-all `app.exception_handler(Exception)(exception_handler)` at line 262, and that handler calls `create_error_response(exc)` which DOES apply `sanitize_message`. However, FastAPI exception handlers fire only on **unhandled** exceptions that propagate out of a route function.\n\nAll affected HTTP paths catch `Exception` *inside* the route coroutine and construct the response themselves:\n\n```python\n# vllm/entrypoints/anthropic/api_router.py:71-81 (POST /v1/messages)\ntry:\n    generator = await handler.create_messages(request, raw_request)\nexcept Exception as e:\n    logger.exception(\"Error in create_messages: %s\", e)\n    return JSONResponse(\n        status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,\n        content=AnthropicErrorResponse(\n            error=AnthropicError(\n                type=\"internal_error\",\n                message=str(e),       # <-- unsanitized\n            )\n        ).model_dump(),\n    )\n```\n\nBecause the exception is caught and a `JSONResponse` is returned in-route, every registered FastAPI exception handler — including the sanitizing global one — is bypassed. The WebSocket path bypasses it for a different reason: WebSocket frames don't traverse FastAPI's HTTP exception handler chain at all.\n\n## Reachability — the same primitive as the parent CVE\n\nThe Anthropic Messages API accepts image content parts in the request body (`type: \"image\"` with base64 `source.data` or `type: \"image_url\"`). Image bytes are passed to the same multimodal loader used by the OpenAI router. Malformed bytes cause `PIL.Image.open` to raise:\n\n```\nUnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x7a95e299e750>\n```\n\nThe exception propagates up through `handler.create_messages` into the `except Exception as e:` at `api_router.py:75`. `str(e)` returns the exception message verbatim, including the address. The address ends up in the `error.message` field of the JSON response body returned to the attacker. ASLR entropy on the affected process drops from ~4 billion to ~8 candidates, identically to CVE-2026-22778 Stage 1.\n\nThe same primitive is reachable on `POST /v1/messages/count_tokens` (route #2), inside the SSE streaming converter when an exception is raised mid-stream (route #3), and over the realtime speech-to-text WebSocket when audio decoder or generation paths raise an exception containing any object repr (routes #4, #5).\n\n## Chronology — these are scope misses, not legacy code\n\n- **2026-01-09:** PR #31987 (`aa125ecf0`) introduces `sanitize_message` and applies it to OpenAI router HTTP exception handlers.\n- **2026-01-15** (six days later): PR #32369 (`4c1c501a7`) adds `vllm/entrypoints/anthropic/api_router.py` containing line 78's `message=str(e)`. The fix was not applied to the new router.\n- **2026-03-02** (~two months later): PR #35588 (`9a87b0578`) adds the Anthropic `count_tokens` endpoint, replicating the same `message=str(e)` pattern at line 124.\n- **2026-05-12** (~four months later): PR #42370 (`d37e25ffb`) consolidates speech-to-text entrypoints and the realtime WebSocket uses `send_error(str(e), ...)` for both error paths.\n- **2026-05-26:** current `main` HEAD, all five lines still present.\n\n## CVSS v3.1\n\n`AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N` — Base 5.3 (MEDIUM)\n\nThe parent CVE-2026-22778 was 9.8 (CRITICAL) because it chained the Stage 1 leak with a Stage 2 libopenjp2 heap overflow. Stage 2 is patched in OpenCV ≥ 4.13.0 (PR #32668 bumped the requirement), so Stage 1 alone here is a partial ASLR-bypass info-disclosure primitive rather than a complete RCE. Deployments that ship vLLM alongside an older OpenCV/libopenjp2 (system OpenCV on long-LTS distros, custom Docker images, downstream rebuilds) re-enable the full chain via the affected endpoints.\n\n## CWE\n\nCWE-532 (Insertion of Sensitive Information into Log File / Error Message). Same CWE assigned to the parent CVE-2026-22778.\n\n## Remediation\n\n### 1. Apply `sanitize_message` symmetrically to the five sites\n\n```python\n# vllm/entrypoints/anthropic/api_router.py — add at top:\nfrom vllm.entrypoints.utils import sanitize_message\n\n# Line 78 (POST /v1/messages) and Line 124 (count_tokens):\nmessage=sanitize_message(str(e)),\n```\n\n```python\n# vllm/entrypoints/anthropic/serving.py — add at top:\nfrom vllm.entrypoints.utils import sanitize_message\n\n# Line 808:\nerror=AnthropicError(type=\"internal_error\", message=sanitize_message(str(e))),\n```\n\n```python\n# vllm/entrypoints/speech_to_text/realtime/connection.py — add at top:\nfrom vllm.entrypoints.utils import sanitize_message\n\n# Lines 75 and 265:\nawait self.send_error(sanitize_message(str(e)), \"processing_error\")\n```\n\n### 2. Tighten the regex (defense in depth)\n\nThe current regex `r\" at 0x[0-9a-f]+>\"` is narrow — it only matches the exact CPython builtin object-repr suffix in lowercase hex with a trailing `>`. Future Python versions, C extensions, or custom `__repr__` methods could produce non-matching formats that re-enable the leak:\n\n```python\n# vllm/entrypoints/utils.py\ndef sanitize_message(message: str) -> str:\n    # Strip any standalone hex address; downstream observers don't need them.\n    return re.sub(r\"\\b0x[0-9a-fA-F]{6,}\\b\", \"0x?\", message)\n```\n\n### 3. Future-proofing: consider a response middleware\n\nBoth the route-local exception handling pattern (Anthropic router) and the WebSocket path bypass FastAPI's exception handler chain. A response-level middleware that always invokes `sanitize_message` on outgoing error bodies would prevent this class of regression entirely.\n\n## Affected versions\n\n- All vLLM versions containing `vllm/entrypoints/anthropic/api_router.py` (introduced 2026-01-15 in PR #32369).\n- All vLLM versions containing `vllm/entrypoints/speech_to_text/realtime/connection.py` (introduced 2026-05-12 in PR #42370).\n- Confirmed present in `main` HEAD `771e1e48b` (2026-05-26).\n\n## References\n\n- Parent advisory: https://github.com/vllm-project/vllm/security/advisories/GHSA-4r2x-xpjr-7cvv (CVE-2026-22778)\n- Fix PRs to model the patch on: #31987, #32319, #32668\n- `vllm/entrypoints/utils.py:sanitize_message`: https://github.com/vllm-project/vllm/blob/771e1e48b/vllm/entrypoints/utils.py#L323-L326\n- `vllm/entrypoints/anthropic/api_router.py` leak site: https://github.com/vllm-project/vllm/blob/771e1e48b/vllm/entrypoints/anthropic/api_router.py#L78\n\n## Steps to reproduce\n\n1. Clone the target: `git clone --depth 1 https://github.com/vllm-project/vllm`\n2. Run the proof of concept (`PoC.py`) against the cloned source.\n3. Observe the result shown under *Verified result* below.\n\n## Credit\n\nKai Aizen — SnailSploit (@SnailSploit). Adversarial & Offensive Security Research.\n\n## Fix\n\nA fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/45119\n\n## Affected packages\n\n- `vllm < 0.24.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `vllm 0.24.0`","depth":"twilight","depthScore":41,"depthScoreParts":{"impact":29.2,"likelihood":0.2,"exploitation":12,"ransomware":0},"changes":[{"seq":201766,"id":"CVE-2026-54236","ts":1789399676082,"field":"cvss","old":null,"new":"5.3"},{"seq":200496,"id":"CVE-2026-54236","ts":1789397341917,"field":"cvss","old":"5.3","new":null},{"seq":198413,"id":"CVE-2026-54236","ts":1789391902622,"field":"cvss","old":null,"new":"5.3"},{"seq":196206,"id":"CVE-2026-54236","ts":1789383548818,"field":"cvss","old":"5.3","new":null},{"seq":195135,"id":"CVE-2026-54236","ts":1789380429851,"field":"cvss","old":null,"new":"5.3"},{"seq":193922,"id":"CVE-2026-54236","ts":1789378452352,"field":"cvss","old":"5.3","new":null},{"seq":192709,"id":"CVE-2026-54236","ts":1789376375814,"field":"cvss","old":null,"new":"5.3"},{"seq":191496,"id":"CVE-2026-54236","ts":1789373355828,"field":"cvss","old":"5.3","new":null},{"seq":190281,"id":"CVE-2026-54236","ts":1789369260627,"field":"cvss","old":null,"new":"5.3"},{"seq":189068,"id":"CVE-2026-54236","ts":1789368227856,"field":"cvss","old":"5.3","new":null},{"seq":187851,"id":"CVE-2026-54236","ts":1789365118310,"field":"cvss","old":null,"new":"5.3"},{"seq":186638,"id":"CVE-2026-54236","ts":1789363221289,"field":"cvss","old":"5.3","new":null},{"seq":185424,"id":"CVE-2026-54236","ts":1789361065509,"field":"cvss","old":null,"new":"5.3"},{"seq":184211,"id":"CVE-2026-54236","ts":1789358099804,"field":"cvss","old":"5.3","new":null},{"seq":182462,"id":"CVE-2026-54236","ts":1789354193326,"field":"cvss","old":null,"new":"5.3"},{"seq":181255,"id":"CVE-2026-54236","ts":1789353082014,"field":"cvss","old":"5.3","new":null},{"seq":180048,"id":"CVE-2026-54236","ts":1789350136954,"field":"cvss","old":null,"new":"5.3"},{"seq":178841,"id":"CVE-2026-54236","ts":1789348046251,"field":"cvss","old":"5.3","new":null},{"seq":177634,"id":"CVE-2026-54236","ts":1789346254270,"field":"cvss","old":null,"new":"5.3"},{"seq":176427,"id":"CVE-2026-54236","ts":1789342969113,"field":"cvss","old":"5.3","new":null},{"seq":174544,"id":"CVE-2026-54236","ts":1789334732278,"field":"cvss","old":null,"new":"5.3"},{"seq":173339,"id":"CVE-2026-54236","ts":1789333463485,"field":"cvss","old":"5.3","new":null},{"seq":172153,"id":"CVE-2026-54236","ts":1789330972391,"field":"cvss","old":null,"new":"5.3"},{"seq":170967,"id":"CVE-2026-54236","ts":1789328564148,"field":"cvss","old":"5.3","new":null},{"seq":169762,"id":"CVE-2026-54236","ts":1789327037028,"field":"cvss","old":null,"new":"5.3"},{"seq":168557,"id":"CVE-2026-54236","ts":1789323619085,"field":"cvss","old":"5.3","new":null},{"seq":167352,"id":"CVE-2026-54236","ts":1789319468923,"field":"cvss","old":null,"new":"5.3"},{"seq":166147,"id":"CVE-2026-54236","ts":1789318504349,"field":"cvss","old":"5.3","new":null},{"seq":164942,"id":"CVE-2026-54236","ts":1789315673846,"field":"cvss","old":null,"new":"5.3"},{"seq":163737,"id":"CVE-2026-54236","ts":1789313424772,"field":"cvss","old":"5.3","new":null},{"seq":162532,"id":"CVE-2026-54236","ts":1789311799732,"field":"cvss","old":null,"new":"5.3"},{"seq":161327,"id":"CVE-2026-54236","ts":1789308482549,"field":"cvss","old":"5.3","new":null},{"seq":160832,"id":"CVE-2026-54236","ts":1789304424620,"field":"cvss","old":null,"new":"5.3"},{"seq":160379,"id":"CVE-2026-54236","ts":1789303950011,"field":"cvss","old":"5.3","new":null},{"seq":159440,"id":"CVE-2026-54236","ts":1789300369001,"field":"cvss","old":null,"new":"5.3"},{"seq":158670,"id":"CVE-2026-54236","ts":1789299442994,"field":"cvss","old":"5.3","new":null},{"seq":157634,"id":"CVE-2026-54236","ts":1789296560664,"field":"cvss","old":null,"new":"5.3"},{"seq":156429,"id":"CVE-2026-54236","ts":1789294542119,"field":"cvss","old":"5.3","new":null},{"seq":155224,"id":"CVE-2026-54236","ts":1789292762887,"field":"cvss","old":null,"new":"5.3"},{"seq":154019,"id":"CVE-2026-54236","ts":1789289543974,"field":"cvss","old":"5.3","new":null},{"seq":152669,"id":"CVE-2026-54236","ts":1789281471379,"field":"cvss","old":null,"new":"5.3"},{"seq":152309,"id":"CVE-2026-54236","ts":1789281061949,"field":"cvss","old":"5.3","new":null},{"seq":151270,"id":"CVE-2026-54236","ts":1789277510137,"field":"cvss","old":null,"new":"5.3"},{"seq":150231,"id":"CVE-2026-54236","ts":1789276045460,"field":"cvss","old":"5.3","new":null},{"seq":149198,"id":"CVE-2026-54236","ts":1789273682170,"field":"cvss","old":null,"new":"5.3"},{"seq":148165,"id":"CVE-2026-54236","ts":1789271068369,"field":"cvss","old":"5.3","new":null},{"seq":146200,"id":"CVE-2026-54236","ts":1789269261749,"field":"cvss","old":null,"new":"5.3"},{"seq":142813,"id":"CVE-2026-54236","ts":1789261325027,"field":"cvss","old":"5.3","new":null},{"seq":141644,"id":"CVE-2026-54236","ts":1789258720405,"field":"cvss","old":null,"new":"5.3"},{"seq":140485,"id":"CVE-2026-54236","ts":1789256557899,"field":"cvss","old":"5.3","new":null}]}