CVE-2026-54236: vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router

Published Jun 17, 2026
·
Updated

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 sanitizemessage 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.

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 sanitizemessage. 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/apirouter.py | 78 | message=str(e), (inside POST /v1/messages exception handler) | | 2 | vllm/entrypoints/anthropic/apirouter.py | 124 | message=str(e), (inside POST /v1/messages/counttokens) | | 3 | vllm/entrypoints/anthropic/serving.py | 808 | error=AnthropicError(type="internalerror", message=str(e)), (SSE streaming converter) | | 4 | vllm/entrypoints/speechtotext/realtime/connection.py | 75 | await self.senderror(str(e), "processingerror") (WebSocket event loop) | | 5 | vllm/entrypoints/speechtotext/realtime/connection.py | 265 | await self.senderror(str(e), "processingerror") (WebSocket generation loop) |

Why the global exception handler does not save these paths

apiserver.py registers a catch-all app.exceptionhandler(Exception)(exceptionhandler) at line 262, and that handler calls createerrorresponse(exc) which DOES apply sanitizemessage. 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:

python vllm/entrypoints/anthropic/apirouter.py:71-81 (POST /v1/messages) try: generator = await handler.createmessages(request, rawrequest) except Exception as e: logger.exception("Error in createmessages: %s", e) return JSONResponse( statuscode=HTTPStatus.INTERNALSERVERERROR.value, content=AnthropicErrorResponse( error=AnthropicError( type="internalerror", message=str(e), # <-- unsanitized ) ).modeldump(), )

Because 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.

Reachability — the same primitive as the parent CVE

The Anthropic Messages API accepts image content parts in the request body (type: "image" with base64 source.data or type: "imageurl"). Image bytes are passed to the same multimodal loader used by the OpenAI router. Malformed bytes cause PIL.Image.open to raise:

UnidentifiedImageError: cannot identify image file <io.BytesIO object at 0x7a95e299e750>

The exception propagates up through handler.createmessages into the except Exception as e: at apirouter.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.

The same primitive is reachable on POST /v1/messages/counttokens (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).

Chronology — these are scope misses, not legacy code

- 2026-01-09: PR #31987 (aa125ecf0) introduces sanitizemessage and applies it to OpenAI router HTTP exception handlers. - 2026-01-15 (six days later): PR #32369 (4c1c501a7) adds vllm/entrypoints/anthropic/apirouter.py containing line 78's message=str(e). The fix was not applied to the new router. - 2026-03-02 (~two months later): PR #35588 (9a87b0578) adds the Anthropic counttokens endpoint, replicating the same message=str(e) pattern at line 124. - 2026-05-12 (~four months later): PR #42370 (d37e25ffb) consolidates speech-to-text entrypoints and the realtime WebSocket uses senderror(str(e), ...) for both error paths. - 2026-05-26: current main HEAD, all five lines still present.

Remediation

1. Apply sanitizemessage symmetrically to the five sites

python vllm/entrypoints/anthropic/apirouter.py — add at top: from vllm.entrypoints.utils import sanitizemessage

Line 78 (POST /v1/messages) and Line 124 (counttokens): message=sanitizemessage(str(e)),

python vllm/entrypoints/anthropic/serving.py — add at top: from vllm.entrypoints.utils import sanitizemessage

Line 808: error=AnthropicError(type="internalerror", message=sanitizemessage(str(e))),

python vllm/entrypoints/speechtotext/realtime/connection.py — add at top: from vllm.entrypoints.utils import sanitizemessage

Lines 75 and 265: await self.senderror(sanitizemessage(str(e)), "processingerror")

2. Tighten the regex (defense in depth)

The 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:

python vllm/entrypoints/utils.py def sanitizemessage(message: str) -> str: # Strip any standalone hex address; downstream observers don't need them. return re.sub(r"\b0x[0-9a-fA-F]{6,}\b", "0x?", message)

3. Future-proofing: consider a response middleware

Both 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 sanitizemessage on outgoing error bodies would prevent this class of regression entirely.

Affected versions

- All vLLM versions containing vllm/entrypoints/anthropic/apirouter.py (introduced 2026-01-15 in PR #32369). - All vLLM versions containing vllm/entrypoints/speechtotext/realtime/connection.py (introduced 2026-05-12 in PR #42370). - Confirmed present in main HEAD 771e1e48b (2026-05-26).

Steps to reproduce

1. Clone the target: git clone --depth 1 https://github.com/vllm-project/vllm 2. Run the proof of concept (PoC.py) against the cloned source. 3. Observe the result shown under Verified result below.

Credit

Kai Aizen — SnailSploit (@SnailSploit). Adversarial & Offensive Security Research.

Fix

A fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/45119

Other sources

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 sanitizemessage 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 sanitizemessage. The unsanitized sites include the Anthropic API router in vllm/entrypoints/anthropic/apirouter.py (the POST /v1/messages and POST /v1/messages/counttokens handlers), the Server-Sent Events streaming converter in vllm/entrypoints/anthropic/serving.py, and the realtime speech-to-text WebSocket in vllm/entrypoints/speechtotext/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.

MITRE

Affected Software

2 affected components
pip/vllm<=0.23.0
vllm vllm<0.23.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/vllm to a version that resolves this vulnerability.

    Fixed in 0.23.1rc0
  2. Configuration

    At vllm/entrypoints/anthropic/api_router.py replace the use of message=str(e) at line 78 with message=sanitize_message(str(e)). Ensure sanitize_message is imported from vllm.entrypoints.utils.

    vllm/entrypoints/anthropic/api_router.py (POST /v1/messages) error message construction = sanitize_message(str(e))
  3. Configuration

    At vllm/entrypoints/anthropic/api_router.py replace the use of message=str(e) at line 124 with message=sanitize_message(str(e)). Ensure sanitize_message is imported from vllm.entrypoints.utils.

    vllm/entrypoints/anthropic/api_router.py (POST /v1/messages/count_tokens) error message construction = sanitize_message(str(e))
  4. Configuration

    At vllm/entrypoints/anthropic/serving.py replace the use of error=AnthropicError(type="internal_error", message=str(e)) at line 808 with error=AnthropicError(type="internal_error", message=sanitize_message(str(e))). Ensure sanitize_message is imported from vllm.entrypoints.utils.

    vllm/entrypoints/anthropic/serving.py (SSE streaming converter) error message construction = AnthropicError(type="internal_error", message=sanitize_message(str(e)))
  5. Configuration

    At vllm/entrypoints/speech_to_text/realtime/connection.py replace await self.send_error(str(e), "processing_error") at line 75 with await self.send_error(sanitize_message(str(e)), "processing_error"). Ensure sanitize_message is imported from vllm.entrypoints.utils.

    vllm/entrypoints/speech_to_text/realtime/connection.py (WebSocket event loop) WebSocket send_error payload = send_error(sanitize_message(str(e)), "processing_error")
  6. Configuration

    At vllm/entrypoints/speech_to_text/realtime/connection.py replace await self.send_error(str(e), "processing_error") at line 265 with await self.send_error(sanitize_message(str(e)), "processing_error"). Ensure sanitize_message is imported from vllm.entrypoints.utils.

    vllm/entrypoints/speech_to_text/realtime/connection.py (WebSocket generation loop) WebSocket send_error payload = send_error(sanitize_message(str(e)), "processing_error")
  7. Configuration

    In vllm/entrypoints/utils.py update the sanitize_message implementation to replace hex heap addresses using re.sub(r"\b0x[0-9a-fA-F]{6,}\b", "0x?", message). Ensure this sanitization is applied consistently to all outgoing error messages.

    vllm/entrypoints/utils.py sanitize_message regex substitution = re.sub(r"\b0x[0-9a-fA-F]{6,}\b", "0x?", message)
  8. Compensating control

    Add a response-level middleware that always invokes vllm.entrypoints.utils.sanitize_message on outgoing error bodies (including JSONResponse and WebSocket error sends) to ensure any unchecked route-local exception text is sanitized before being sent to clients.

Event History

Jun 17, 2026
Advisory Published
via GitHub·02:04 PM
Data Sourced
via GitHub·02:04 PM
DescriptionWeaknessAffected Software
Jun 22, 2026
CVE Published
via MITRE·10:09 PM
Data Sourced
via MITRE·10:09 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·11:16 PM
RemedyDescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-54236?

The severity of CVE-2026-54236 is rated as medium with a CVSS score of 5.3.

2

What impact does CVE-2026-54236 have on systems?

CVE-2026-54236 can lead to the leaking of PIL repr addresses via the Anthropic API router.

3

How do I fix CVE-2026-54236?

To fix CVE-2026-54236, you should update your vLLM software to the latest version that includes the security patch.

4

What types of vulnerabilities are associated with CVE-2026-54236?

CVE-2026-54236 is associated with incomplete fixes from a previous vulnerability, specifically CVE-2026-22778.

5

Is CVE-2026-54236 remotely exploitable?

Yes, CVE-2026-54236 is classified as having remote attack vectors.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203