From 49669da5315bdf6134820a4a39e1f2051530253a Mon Sep 17 00:00:00 2001 From: MANGESH NAGNATWALSANE Date: Fri, 23 Oct 2026 06:44:08 +0000 Subject: [PATCH 1/2] feat(vllm): add testbed for CVE-2026-22778 --- vllm/CVE-2026-22778/Dockerfile | 9 ++ vllm/CVE-2026-22778/README.md | 67 ++++++++++++ vllm/CVE-2026-22778/docker-compose.yml | 28 +++++ vllm/CVE-2026-22778/server.py | 139 +++++++++++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 vllm/CVE-2026-22778/Dockerfile create mode 100644 vllm/CVE-2026-22778/README.md create mode 100644 vllm/CVE-2026-22778/docker-compose.yml create mode 100644 vllm/CVE-2026-22778/server.py diff --git a/vllm/CVE-2026-22778/Dockerfile b/vllm/CVE-2026-22778/Dockerfile new file mode 100644 index 00000000..0134596c --- /dev/null +++ b/vllm/CVE-2026-22778/Dockerfile @@ -0,0 +1,9 @@ +FROM docker.io/library/python:3.11-slim + +WORKDIR /app + +RUN pip install --no-cache-dir fastapi uvicorn pillow pydantic + +COPY server.py /app/server.py + +CMD ["python", "server.py"] diff --git a/vllm/CVE-2026-22778/README.md b/vllm/CVE-2026-22778/README.md new file mode 100644 index 00000000..4e4f4166 --- /dev/null +++ b/vllm/CVE-2026-22778/README.md @@ -0,0 +1,67 @@ +# CVE-2026-22778: vLLM Multimodal Endpoint Heap Memory Address Disclosure + +vLLM versions from `0.8.3` to before `0.14.1` contain a sensitive information disclosure vulnerability (CWE-209 / CWE-532). When an invalid image payload is sent to vLLM's OpenAI-compatible multimodal endpoint (`/v1/chat/completions`), Python's Pillow (`PIL`) library fails to decode the image and raises an `UnidentifiedImageError`. vLLM directly reflects this exception string back in the HTTP 400 response, leaking the raw heap memory address (`cannot identify image file <_io.BytesIO object at 0x...>`). This leak significantly reduces ASLR entropy and can be chained with memory corruption vulnerabilities in native image decoders (such as OpenCV or OpenJPEG) to achieve Remote Code Execution (RCE). + +In vLLM `0.14.1` ([PR #31987](https://github.com/vllm-project/vllm/pull/31987)), the response is sanitized using `re.sub(r" at 0x[0-9a-f]+>", ">", message)` to strip memory addresses. + +## Lab Architecture + +This lab runs lightweight, CPU-compatible containers replicating vLLM's exact multimodal ingestion layer and error handling: +- **Vulnerable (`vllm-vuln`):** Version `0.14.0` (HTTP port `8000`) +- **Patched (`vllm-patched`):** Version `0.14.1` (HTTP port `8001`) + +Inside each container, `server.py` handles the OpenAI-compatible endpoints (`/v1/models`, `/v1/chat/completions`, `/version`, `/health`), using real Pillow decoding so the vulnerability triggers on CPU without requiring 30 GB of GPU images and model weights. + +## Setup & Running + +Start both containers: + +```sh +docker compose up -d --build +``` + +## Vulnerability Verification + +### 1. Test Vulnerable Instance (v0.14.0 - Port 8000) + +Send an invalid base64 image payload to `/v1/chat/completions`: + +```sh +curl -s -X POST http://127.0.0.1:8000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"llava-hf/llava-1.5-7b-hf","messages":[{"role":"user","content":[{"type":"text","text":"describe"},{"type":"image_url","image_url":{"url":"data:image/png;base64,bm90YW5pbWFnZQ=="}}]}],"max_tokens":1}' +``` + +**Expected Response (Vulnerable - Leaks Heap Pointer):** +```json +{ + "error": { + "message": "cannot identify image file <_io.BytesIO object at 0x7fa890123450>", + "type": "BadRequest", + "param": "messages", + "code": 400 + } +} +``` + +### 2. Test Patched Instance (v0.14.1 - Port 8001) + +Send the identical request to port `8001`: + +```sh +curl -s -X POST http://127.0.0.1:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"llava-hf/llava-1.5-7b-hf","messages":[{"role":"user","content":[{"type":"text","text":"describe"},{"type":"image_url","image_url":{"url":"data:image/png;base64,bm90YW5pbWFnZQ=="}}]}],"max_tokens":1}' +``` + +**Expected Response (Patched - Heap Address Sanitized):** +```json +{ + "error": { + "message": "cannot identify image file <_io.BytesIO object>", + "type": "BadRequest", + "param": "messages", + "code": 400 + } +} +``` diff --git a/vllm/CVE-2026-22778/docker-compose.yml b/vllm/CVE-2026-22778/docker-compose.yml new file mode 100644 index 00000000..8c2e8b0b --- /dev/null +++ b/vllm/CVE-2026-22778/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + vllm-vuln: + build: + context: . + dockerfile: Dockerfile + container_name: cve-2026-22778-vuln + ports: + - "8000:8000" + environment: + - PORT=8000 + - VLLM_VERSION=0.14.0 + - IS_PATCHED=false + restart: unless-stopped + + vllm-patched: + build: + context: . + dockerfile: Dockerfile + container_name: cve-2026-22778-patched + ports: + - "8001:8001" + environment: + - PORT=8001 + - VLLM_VERSION=0.14.1 + - IS_PATCHED=true + restart: unless-stopped diff --git a/vllm/CVE-2026-22778/server.py b/vllm/CVE-2026-22778/server.py new file mode 100644 index 00000000..a54bd7b1 --- /dev/null +++ b/vllm/CVE-2026-22778/server.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +vLLM Service for CVE-2026-22778 Validation. +Replicates vLLM's OpenAI-compatible HTTP API and Pillow image decoding error handling. +Supports CPU mode without heavy GPU weights. +""" + +import base64 +import os +import re +from io import BytesIO +from typing import Any, List, Optional, Union + +import uvicorn +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from PIL import Image, UnidentifiedImageError +from pydantic import BaseModel, ConfigDict + +VLLM_VERSION = os.environ.get("VLLM_VERSION", "0.14.0") +PORT = int(os.environ.get("PORT", "8000")) +IS_PATCHED = os.environ.get("IS_PATCHED", "false").lower() in ("true", "1", "yes") + +app = FastAPI(title="vLLM OpenAI Compatible Server", version=VLLM_VERSION) + + +def sanitize_message(message: str) -> str: + """Sanitize message as implemented in vLLM PR #31987 (0.14.1+ fix).""" + return re.sub(r" at 0x[0-9a-f]+>", ">", message) + + +class ImageUrl(BaseModel): + url: str + model_config = ConfigDict(extra="allow") + + +class ContentPart(BaseModel): + type: str + text: Optional[str] = None + image_url: Optional[ImageUrl] = None + model_config = ConfigDict(extra="allow") + + +class ChatMessage(BaseModel): + role: str + content: Union[str, List[ContentPart]] + model_config = ConfigDict(extra="allow") + + +class ChatCompletionRequest(BaseModel): + model: Optional[str] = "llava-hf/llava-1.5-7b-hf" + messages: List[ChatMessage] + model_config = ConfigDict(extra="allow") + + +@app.get("/version") +async def get_version(): + return {"version": VLLM_VERSION} + + +@app.get("/health") +async def health(): + return {"status": "ok"} + + +@app.get("/v1/models") +async def get_models(): + return { + "object": "list", + "data": [ + { + "id": "llava-hf/llava-1.5-7b-hf", + "object": "model", + "owned_by": "vllm", + "root": "llava-hf/llava-1.5-7b-hf" + } + ] + } + + +@app.post("/v1/chat/completions") +async def chat_completions(req: ChatCompletionRequest): + # Process multimodal input messages + for msg in req.messages: + if isinstance(msg.content, list): + for part in msg.content: + if part.type == "image_url" and part.image_url: + url = part.image_url.url + if url.startswith("data:image"): + # Extract base64 encoded data + try: + _, b64data = url.split(",", 1) + except ValueError: + b64data = url + else: + b64data = url + + try: + raw_bytes = base64.b64decode(b64data) + except Exception: + raw_bytes = b64data.encode("utf-8") + + try: + # Replicates vllm.multimodal.image.ImageMediaIO.load_bytes() + buf = BytesIO(raw_bytes) + img = Image.open(buf) + img.verify() + except UnidentifiedImageError as exc: + error_msg = str(exc) + if IS_PATCHED or VLLM_VERSION >= "0.14.1": + error_msg = sanitize_message(error_msg) + + return JSONResponse( + status_code=400, + content={ + "error": { + "message": error_msg, + "type": "BadRequest", + "param": "messages", + "code": 400 + } + } + ) + + return { + "id": "chatcmpl-test", + "object": "chat.completion", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Image processed successfully."}, + "finish_reason": "stop" + } + ] + } + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=PORT) From 1bc572c1e3c727cdd5c1011c8d9c8ff4d630ccd6 Mon Sep 17 00:00:00 2001 From: MANGESH NAGNATWALSANE Date: Fri, 23 Oct 2026 06:55:10 +0000 Subject: [PATCH 2/2] docs(vllm): add rationale on why official docker images are not feasible --- vllm/CVE-2026-22778/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vllm/CVE-2026-22778/README.md b/vllm/CVE-2026-22778/README.md index 4e4f4166..9893adbe 100644 --- a/vllm/CVE-2026-22778/README.md +++ b/vllm/CVE-2026-22778/README.md @@ -12,6 +12,11 @@ This lab runs lightweight, CPU-compatible containers replicating vLLM's exact mu Inside each container, `server.py` handles the OpenAI-compatible endpoints (`/v1/models`, `/v1/chat/completions`, `/version`, `/health`), using real Pillow decoding so the vulnerability triggers on CPU without requiring 30 GB of GPU images and model weights. +### Why Official vLLM Images Are Not Feasible +1. **CUDA / GPU Dependency:** Official vLLM container images (`vllm/vllm-openai:v0.14.0`) are ~9 GB compressed (~20 GB uncompressed) and require NVIDIA CUDA GPUs. They crash on startup on standard CPU environments due to PyTorch/Triton CUDA checks. Official CPU images (`vllm-openai-cpu`) were only introduced in v0.16.0+. +2. **Mandatory 15+ GB Model Weights:** Official `vllm serve` strictly requires loading full model weights (e.g. `llava-hf/llava-1.5-7b-hf`) before binding to HTTP ports, which is unviable for lightweight scanner testbeds. +3. **Pre-Inference Vulnerability Path:** The vulnerability exists exclusively in the HTTP request parsing and Pillow image decoding step (`Image.open(BytesIO(...))`) before inference. Running `server.py` with real Pillow and FastAPI reproduces the exact HTTP request parsing, error reflection, and address leak without unnecessary hardware and weight overhead. + ## Setup & Running Start both containers: