{"id":"CVE-2026-54236","aliases":["EUVD-2026-38403","GHSA-hgg8-fqqc-vfmw","PYSEC-2026-3408"],"status":"published","title":"vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router","description":"vLLM is an inference and serving engine for large language models (LLMs). Prior to 0.23.1rc0, the fix for CVE-2026-22778, which introduced a sanitize_message helper that strips object-repr memory addresses from error messages before they reach the client, is incomplete: several response paths echo str(exc) directly to clients without calling sanitize_message. The unsanitized sites include the Anthropic API router in vllm/entrypoints/anthropic/api_router.py (the POST /v1/messages and POST /v1/messages/count_tokens handlers), the Server-Sent Events streaming converter in vllm/entrypoints/anthropic/serving.py, and the realtime speech-to-text WebSocket in vllm/entrypoints/speech_to_text/realtime/connection.py. These paths catch the exception inside the route coroutine and construct the JSONResponse themselves, bypassing the sanitizing global FastAPI exception handler, and WebSocket frames do not traverse that handler chain at all. Using the same primitive as the parent issue, an unauthenticated attacker can send malformed image bytes through the Anthropic Messages API image content parts so that PIL.Image.open raises an UnidentifiedImageError whose message contains the BytesIO object repr, leaking the heap memory address verbatim in the error.message field of the response body. This vulnerability is fixed in 0.23.1rc0.","severity":{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00927,"percentile":0.608,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-532"],"published":"2026-06-17T14:04:09Z","modified":"2026-10-28T02:30:29Z","exploits":[{"source":"nuclei","id":"CVE-2026-54236","title":"vLLM \u003c= 0.23.0 - Anthropic Router Heap Address Information Leak","type":"detection template","url":"https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2026/CVE-2026-54236.yaml"}],"fixes":[{"ecosystem":"PyPI","package":"vllm","fixed_in":["0.24.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"vllm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.23.0"},{"fixed":"0.24.0"}]}]},{"source":"osv","ecosystem":"PyPI","package":"vllm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.24.0"}]}],"versions":["0.0.1","0.1.0","0.1.1","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.10.0","0.10.1","0.10.1.1","0.10.2","0.11.0","0.11.1","0.11.2","0.12.0","0.13.0","0.14.0","0.14.1","0.15.0","0.15.1","0.16.0","0.17.0","0.17.1","0.18.0","0.18.1","0.19.0","0.19.1","0.2.0","0.2.1","0.2.1.post1","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6","0.2.7","0.20.0","0.20.1","0.20.2","0.21.0","0.22.0","0.22.1","0.23.0","0.3.0","0.3.1","0.3.2","0.3.3","0.4.0","0.4.0.post1","0.4.1","0.4.2","0.4.3","0.5.0","0.5.0.post1","0.5.1","0.5.2","0.5.3","0.5.3.post1","0.5.4","0.5.5","0.6.0","0.6.1","0.6.1.post1","0.6.1.post2","0.6.2","0.6.3","0.6.3.post1","0.6.4","0.6.4.post1","0.6.5","0.6.6","0.6.6.post1","0.7.0","0.7.1","0.7.2","0.7.3","0.8.0","0.8.1","0.8.2","0.8.3","0.8.4","0.8.5","0.8.5.post1","0.9.0","0.9.0.1","0.9.1","0.9.2"]},{"source":"osv","ecosystem":"PyPI","package":"vllm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.23.0"}]}],"versions":["0.0.1","0.1.0","0.1.1","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.10.0","0.10.1","0.10.1.1","0.10.2","0.11.0","0.11.1","0.11.2","0.12.0","0.13.0","0.14.0","0.14.1","0.15.0","0.15.1","0.16.0","0.17.0","0.17.1","0.18.0","0.18.1","0.19.0","0.19.1","0.2.0","0.2.1","0.2.1.post1","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6","0.2.7","0.20.0","0.20.1","0.20.2","0.21.0","0.22.0","0.22.1","0.23.0","0.3.0","0.3.1","0.3.2","0.3.3","0.4.0","0.4.0.post1","0.4.1","0.4.2","0.4.3","0.5.0","0.5.0.post1","0.5.1","0.5.2","0.5.3","0.5.3.post1","0.5.4","0.5.5","0.6.0","0.6.1","0.6.1.post1","0.6.1.post2","0.6.2","0.6.3","0.6.3.post1","0.6.4","0.6.4.post1","0.6.5","0.6.6","0.6.6.post1","0.7.0","0.7.1","0.7.2","0.7.3","0.8.0","0.8.1","0.8.2","0.8.3","0.8.4","0.8.5","0.8.5.post1","0.9.0","0.9.0.1","0.9.1","0.9.2"]}],"products":[{"vendor":"vllm","product":"vllm"},{"vendor":"vllm-project","product":"vllm"}],"references":[{"url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-hgg8-fqqc-vfmw","tags":["x_refsource_CONFIRM","exploit","Exploit","Third Party Advisory","advisory","web"]},{"url":"https://github.com/vllm-project/vllm/pull/45119","tags":["x_refsource_MISC","Issue Tracking","fix","web"]},{"url":"https://github.com/vllm-project/vllm/commit/94923629729381d7f7c9efde72071a2441f7fd82","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-54236","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-hgg8-fqqc-vfmw","tags":["advisory"]},{"url":"https://github.com/pypa/advisory-database/tree/main/vulns/vllm/PYSEC-2026-3408.yaml","tags":["web"]},{"url":"https://github.com/vllm-project/vllm","tags":["package"]},{"url":"https://pypi.org/project/vllm","tags":["web","package"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/54xxx/CVE-2026-54236.json","tags":["advisory"]}],"sources":{"cve":[{"id":"CVE-2026-54236","title":"vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router","description":"vLLM is an inference and serving engine for large language models (LLMs). Prior to 0.23.1rc0, the fix for CVE-2026-22778, which introduced a sanitize_message helper that strips object-repr memory addresses from error messages before they reach the client, is incomplete: several response paths echo str(exc) directly to clients without calling sanitize_message. The unsanitized sites include the Anthropic API router in vllm/entrypoints/anthropic/api_router.py (the POST /v1/messages and POST /v1/messages/count_tokens handlers), the Server-Sent Events streaming converter in vllm/entrypoints/anthropic/serving.py, and the realtime speech-to-text WebSocket in vllm/entrypoints/speech_to_text/realtime/connection.py. These paths catch the exception inside the route coroutine and construct the JSONResponse themselves, bypassing the sanitizing global FastAPI exception handler, and WebSocket frames do not traverse that handler chain at all. Using the same primitive as the parent issue, an unauthenticated attacker can send malformed image bytes through the Anthropic Messages API image content parts so that PIL.Image.open raises an UnidentifiedImageError whose message contains the BytesIO object repr, leaking the heap memory address verbatim in the error.message field of the response body. This vulnerability is fixed in 0.23.1rc0.","status":"published","published":"2026-06-22T22:09:15.034Z","modified":"2026-06-23T12:33:28.108Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-532"],"updated_at":"2026-11-02T13:37:58.613221Z"}],"euvd":[{"id":"EUVD-2026-38403","description":"vLLM is an inference and serving engine for large language models (LLMs). Prior to 0.23.1rc0, the fix for CVE-2026-22778, which introduced a sanitize_message helper that strips object-repr memory addresses from error messages before they reach the client, is incomplete: several response paths echo str(exc) directly to clients without calling sanitize_message. The unsanitized sites include the Anthropic API router in vllm/entrypoints/anthropic/api_router.py (the POST /v1/messages and POST /v1/messages/count_tokens handlers), the Server-Sent Events streaming converter in vllm/entrypoints/anthropic/serving.py, and the realtime speech-to-text WebSocket in vllm/entrypoints/speech_to_text/realtime/connection.py. These paths catch the exception inside the route coroutine and construct the JSONResponse themselves, bypassing the sanitizing global FastAPI exception handler, and WebSocket frames do not traverse that handler chain at all. Using the same primitive as the parent issue, an unauthenticated attacker can send malformed image bytes through the Anthropic Messages API image content parts so that PIL.Image.open raises an UnidentifiedImageError whose message contains the BytesIO object repr, leaking the heap memory address verbatim in the error.message field of the response body. This vulnerability is fixed in 0.23.1rc0.","status":"published","published":"2026-06-22T22:09:15Z","modified":"2026-06-23T12:33:28Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T18:20:33.324654Z"}],"ghsa":[{"id":"GHSA-hgg8-fqqc-vfmw","title":"vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router","description":"# vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via the Anthropic API router\n\n**Researcher:** Kai Aizen — SnailSploit (@SnailSploit), Adversarial \u0026 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 (`\u003c_io.BytesIO object at 0x7a95e299e750\u003e` → `\u003c_io.BytesIO object\u003e`) 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),       # \u003c-- 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 \u003c_io.BytesIO object at 0x7a95e299e750\u003e\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]+\u003e\"` is narrow — it only matches the exact CPython builtin object-repr suffix in lowercase hex with a trailing `\u003e`. 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) -\u003e 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 \u0026 Offensive Security Research.\n\n## Fix\n\nA fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/45119","status":"published","published":"2026-06-17T14:04:09Z","modified":"2026-07-20T13:36:38Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-532"],"updated_at":"2026-11-02T14:40:46.257074Z"}],"nvd":[{"id":"CVE-2026-54236","description":"vLLM is an inference and serving engine for large language models (LLMs). Prior to 0.23.1rc0, the fix for CVE-2026-22778, which introduced a sanitize_message helper that strips object-repr memory addresses from error messages before they reach the client, is incomplete: several response paths echo str(exc) directly to clients without calling sanitize_message. The unsanitized sites include the Anthropic API router in vllm/entrypoints/anthropic/api_router.py (the POST /v1/messages and POST /v1/messages/count_tokens handlers), the Server-Sent Events streaming converter in vllm/entrypoints/anthropic/serving.py, and the realtime speech-to-text WebSocket in vllm/entrypoints/speech_to_text/realtime/connection.py. These paths catch the exception inside the route coroutine and construct the JSONResponse themselves, bypassing the sanitizing global FastAPI exception handler, and WebSocket frames do not traverse that handler chain at all. Using the same primitive as the parent issue, an unauthenticated attacker can send malformed image bytes through the Anthropic Messages API image content parts so that PIL.Image.open raises an UnidentifiedImageError whose message contains the BytesIO object repr, leaking the heap memory address verbatim in the error.message field of the response body. This vulnerability is fixed in 0.23.1rc0.","status":"published","published":"2026-06-22T23:16:31.29Z","modified":"2026-10-25T23:10:00.167Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-532"],"updated_at":"2026-11-02T14:12:25.288279Z"}],"osv":[{"id":"CVE-2026-54236","title":"vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router","description":"vLLM is an inference and serving engine for large language models (LLMs). Prior to 0.23.1rc0, the fix for CVE-2026-22778, which introduced a sanitize_message helper that strips object-repr memory addresses from error messages before they reach the client, is incomplete: several response paths echo str(exc) directly to clients without calling sanitize_message. The unsanitized sites include the Anthropic API router in vllm/entrypoints/anthropic/api_router.py (the POST /v1/messages and POST /v1/messages/count_tokens handlers), the Server-Sent Events streaming converter in vllm/entrypoints/anthropic/serving.py, and the realtime speech-to-text WebSocket in vllm/entrypoints/speech_to_text/realtime/connection.py. These paths catch the exception inside the route coroutine and construct the JSONResponse themselves, bypassing the sanitizing global FastAPI exception handler, and WebSocket frames do not traverse that handler chain at all. Using the same primitive as the parent issue, an unauthenticated attacker can send malformed image bytes through the Anthropic Messages API image content parts so that PIL.Image.open raises an UnidentifiedImageError whose message contains the BytesIO object repr, leaking the heap memory address verbatim in the error.message field of the response body. This vulnerability is fixed in 0.23.1rc0.","status":"published","published":"2026-06-22T22:09:15.034Z","modified":"2026-10-28T02:30:29.356715661Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-532"],"updated_at":"2026-11-02T15:45:10.818486Z"},{"id":"GHSA-hgg8-fqqc-vfmw","title":"vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router","description":"# vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via the Anthropic API router\n\n**Researcher:** Kai Aizen — SnailSploit (@SnailSploit), Adversarial \u0026 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 (`\u003c_io.BytesIO object at 0x7a95e299e750\u003e` → `\u003c_io.BytesIO object\u003e`) 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),       # \u003c-- 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 \u003c_io.BytesIO object at 0x7a95e299e750\u003e\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]+\u003e\"` is narrow — it only matches the exact CPython builtin object-repr suffix in lowercase hex with a trailing `\u003e`. 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) -\u003e 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 \u0026 Offensive Security Research.\n\n## Fix\n\nA fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/45119","status":"published","published":"2026-06-17T14:04:09Z","modified":"2026-09-10T03:50:49.787104004Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-532"],"updated_at":"2026-11-02T15:46:49.28217Z"},{"id":"PYSEC-2026-3408","title":"vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router","description":"# vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via the Anthropic API router\n\n**Researcher:** Kai Aizen — SnailSploit (@SnailSploit), Adversarial \u0026 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 (`\u003c_io.BytesIO object at 0x7a95e299e750\u003e` → `\u003c_io.BytesIO object\u003e`) 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),       # \u003c-- 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 \u003c_io.BytesIO object at 0x7a95e299e750\u003e\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\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]+\u003e\"` is narrow — it only matches the exact CPython builtin object-repr suffix in lowercase hex with a trailing `\u003e`. 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) -\u003e 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\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 \u0026 Offensive Security Research.\n\n## Fix\n\nA fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/45119","status":"published","published":"2026-07-13T15:46:18.717048Z","modified":"2026-07-13T16:31:34.170341278Z","updated_at":"2026-11-02T15:52:56.847083Z"}]}}
