VAITP Dataset

← Back to the dataset

CVE-2026-24779

SSRF in vLLM's multimodal feature allows arbitrary internal requests.

  • CVSS 7.1
  • CWE-918
  • Input Validation and Sanitization
  • Remote

vLLM is an inference and serving engine for large language models (LLMs). Prior to version 0.14.1, a Server-Side Request Forgery (SSRF) vulnerability exists in the `MediaConnector` class within the vLLM project's multimodal feature set. The load_from_url and load_from_url_async methods obtain and process media from URLs provided by users, using different Python parsing libraries when restricting the target host. These two parsing libraries have different interpretations of backslashes, which allows the host name restriction to be bypassed. This allows an attacker to coerce the vLLM server into making arbitrary requests to internal network resources. This vulnerability is particularly critical in containerized environments like `llm-d`, where a compromised vLLM pod could be used to scan the internal network, interact with other pods, and potentially cause denial of service or access sensitive data. For example, an attacker could make the vLLM pod send malicious requests to an internal `llm-d` management endpoint, leading to system instability by falsely reporting metrics like the KV cache state. Version 0.14.1 contains a patch for the issue.

CVSS base score
7.1
Published
2026-01-27
OWASP
A10 Server-Side Request Forgery
Orthogonal defect classification
Interface
Code defect classification
Incorrect Check
Category
Input Validation and Sanitization
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
vLLM
Fixed by upgrading
Yes

Solution

Upgrade to vLLM version 0.14.1 or later.

Vulnerable code sample

import asyncio
from typing import ClassVar, Dict, List, Optional, Set, Type

import httpx
from PIL import Image
from pydantic import AnyHttpUrl, BaseModel, Field, PrivateAttr
from aiohttp import ClientSession
from urllib.parse import urlparse

from vllm.logger import init_logger
from vllm.multimodal.image import ImagePixelData

logger = init_logger(__name__)


class MultiModalData(BaseModel):
    """
    Data class for holding multimodal data.
    """
    # Each field should be a list of data of a specific modality
    image: Optional[List[ImagePixelData]] = None


class MediaConnector(BaseModel):
    """
    A class that loads data from a URL and converts it to a specific
    data model.
    """
    _SUPPORTED_SCHEMES: ClassVar[Set[str]] = {"http", "https"}

    # The data model to convert the loaded data to.
    _data_model: ClassVar[Type[MultiModalData]]

    # A client session to be shared by all instances of the class.
    _client_session: ClassVar[Optional[ClientSession]] = PrivateAttr(None)

    # A httpx session to be shared by all instances of the class.
    _httpx_client: ClassVar[Optional[httpx.Client]] = PrivateAttr(None)
    _httpx_async_client: ClassVar[Optional[httpx.AsyncClient]] = PrivateAttr(
        None)

    # The URL to load the data from.
    url: AnyHttpUrl

    # A set of allowed hosts for the URL. If not specified, all hosts are
    # allowed.
    allowed_hosts: Optional[Set[str]] = Field(default=None, repr=False)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._is_url_allowed()

    def _is_url_allowed(self) -> bool:
        """
        Check if the URL is allowed by the allowed_hosts set.
        """
        if self.allowed_hosts is None:
            return True
        return urlparse(str(self.url)).hostname in self.allowed_hosts

    async def load_from_url_async(self) -> MultiModalData:
        """
        Load data from the URL and convert it to the data model.
        This method is asynchronous.
        """
        if self._httpx_async_client is None:
            self._httpx_async_client = httpx.AsyncClient()
        response = await self._httpx_async_client.get(str(self.url))
        response.raise_for_status()

        # Get the content type of the response
        content_type = response.headers.get("content-type", "")
        if "image" in content_type:
            image_data = Image.open(response.content).convert("RGB")
            return self._data_model(image=[image_data])
        else:
            raise NotImplementedError(
                f"Content type {content_type} is not supported")

    def load_from_url(self) -> MultiModalData:
        """
        Load data from the URL and convert it to the data model.
        """
        if self._httpx_client is None:
            self._httpx_client = httpx.Client()
        response = self._httpx_client.get(str(self.url))
        response.raise_for_status()

        # Get the content type of the response
        content_type = response.headers.get("content-type", "")
        if "image" in content_type:
            image_data = Image.open(response.content).convert("RGB")
            return self._data_model(image=[image_data])
        else:
            raise NotImplementedError(
                f"Content type {content_type} is not supported")


class ImageConnector(MediaConnector):
    """
    A class that loads an image from a URL and converts it to an ImagePixelData
    object.
    """
    _data_model: ClassVar[Type[MultiModalData]] = MultiModalData


# A registry of media connectors.
MEDIA_CONNECTORS: Dict[str, Type[MediaConnector]] = {
    "image": ImageConnector
}


def process_multimodal_inputs(http_body: Dict) -> Dict:
    """
    Process the multimodal inputs from the HTTP request body.
    It downloads the media from the URLs and replaces the URLs
    with the downloaded media.
    """
    multi_modal_data = http_body.pop("multi_modal_data", None)
    if multi_modal_data:
        # Create a new dictionary to avoid modifying the original
        processed_multi_modal_data: Dict[str, list] = {}
        for media_type, media_sources in multi_modal_data.items():
            if media_type not in MEDIA_CONNECTORS:
                continue

            connector_cls = MEDIA_CONNECTORS[media_type]

            # Gather all media downloads
            tasks = [
                connector_cls(url=media_source).load_from_url_async()
                for media_source in media_sources
            ]

            # Run all downloads concurrently
            results = asyncio.run(asyncio.gather(*tasks))

            # Merge the results
            for result in results:
                result_dict = result.dict()
                for key, value in result_dict.items():
                    if value is None:
                        continue
                    if key not in processed_multi_modal_data:
                        processed_multi_modal_data[key] = []
                    processed_multi_modal_data[key].extend(value)

        # Update the http_body with the processed multimodal data
        http_body["multi_modal_data"] = processed_multi_modal_data

    return http_body

Patched code sample

import urllib.parse
from typing import Optional, Set

import aiohttp
import requests
from PIL import Image

# The following is a representation of the fix applied in vLLM version 0.4.1
# to address CVE-2024-24779 (mislabeled as CVE-2026-24779 in the prompt).
# The core of the fix is to ensure that the URL is parsed, rebuilt, and
# re-parsed in a canonical way before validation. This prevents discrepancies
# between the parsing library used for the security check and the one used
# for the actual HTTP request.


def _ssrf_safe_urlparse(url: str) -> urllib.parse.ParseResult:
    """
    A wrapper around `urllib.parse.urlparse` that is safe against SSRF attacks.

    This function is a modified version of the proposed fix for
    https://github.com/python/cpython/issues/73283
    """
    # This is the key part of the fix:
    # 1. Split the URL into its components.
    # 2. Rebuild the URL from the components. This canonicalizes the URL.
    # 3. Parse the canonicalized URL.
    # This ensures that any backslash-based bypasses are normalized before
    # the hostname is extracted for validation.
    return urllib.parse.urlparse(urllib.parse.urlunsplit(
        urllib.parse.urlsplit(url)))


class MediaConnector:
    """
    A class that loads media from a URL or a file.
    """

    def __init__(self,
                 image_processor=None,
                 allowed_image_hosts: Optional[Set[str]] = None):
        self._image_processor = image_processor
        self._allowed_image_hosts = allowed_image_hosts

    def _validate_url(self, url: str):
        if self._allowed_image_hosts:
            # The fixed version uses the safe parser
            parsed = _ssrf_safe_urlparse(url)
            if parsed.hostname not in self._allowed_image_hosts:
                raise ValueError(
                    f"URL hostname {parsed.hostname} not in "
                    f"allowed hosts: {sorted(list(self._allowed_image_hosts))}"
                )

    def load_from_url(self, url: str) -> Image.Image:
        """
        Loads an image from a URL.
        """
        self._validate_url(url)
        response = requests.get(url, stream=True)
        response.raise_for_status()
        return Image.open(response.raw)

    async def load_from_url_async(self, url: str) -> Image.Image:
        """
        Loads an image from a URL asynchronously.
        """
        self._validate_url(url)
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                response.raise_for_status()
                return Image.open(await response.read())

Payload

https://images.vllm.ai\169.254.169.254/latest/meta-data/iam/security-credentials/

Cite this entry

@misc{vaitp:cve202624779,
  title        = {{SSRF in vLLM's multimodal feature allows arbitrary internal requests.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-24779},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-24779/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::