vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router
vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via the Anthropic API router
Researcher: Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research
Severity: CVSS 3.1 5.3 (Medium) AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Target: https://github.com/vllm-project/vllm
Summary
The 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 (→) before error messages reach the client, defeating the ASLR-bypass primitive that CVE-2026-22778 chained with a libopenjp2 heap overflow for RCE.
The 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.
All five lines below are present in main HEAD (771e1e48b, 2026-05-26).
Affected sites
Current main HEAD (771e1e48b, 2026-05-26):
| # | File | Line | Code |
|---|---|---|---|
| 1 | vllm/entrypoints/anthropic/api_router.py |
78 | message=str(e), (inside POST /v1/messages exception handler) |
| 2 | vllm/entrypoints/anthropic/api_router.py |
124 | message=str(e), (inside POST /v1/messages/count_tokens) |
| 3 | vllm/entrypoints/anthropic/serving.py |
808 | error=AnthropicError(type="internal_error", message=str(e)), (SSE streaming converter) |
| 4 | vllm/entrypoints/speech_to_text/realtime/connection.py |
75 | await self.send_error(str(e), "processing_error") (WebSocket event loop) |
| 5 | vllm/entrypoints/speech_to_text/realtime/connection.py |
265 | await self.send_error(str(e), "processing_error") (WebSocket generation loop) |
Why the global exception handler does not save these paths
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.
All affected HTTP paths catch Exception inside the route coroutine and construct the response themselves:
# vllm/entrypoints/anthropic/api_router.py:71-81 (POST /v1/messages)
try:
generator = await handler.create_messages(request, raw_request)
except Exception as e:
logger.exception("Error in create_messages: %s", e)
return JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
content=AnthropicErrorResponse(
error=AnthropicError(
type="internal_error",
message=str(e), #