VYPR
High severityOSV Advisory· Published Jan 27, 2026· Updated Jan 28, 2026

vLLM vulnerable to Server-Side Request Forgery (SSRF) in `MediaConnector`

CVE-2026-24779

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vllmPyPI
< 0.14.10.14.1

Affected products

1
  • Range: submission, v0.1.0, v0.1.1, …

Patches

1
f46d576c54fb

[Misc] Replace urllib's `urlparse` with urllib3's `parse_url` (#32746)

https://github.com/vllm-project/vllmIsotr0pyJan 22, 2026via ghsa
4 files changed · +23 16
  • vllm/connections.py+2 2 modified
    @@ -3,10 +3,10 @@
     
     from collections.abc import Mapping, MutableMapping
     from pathlib import Path
    -from urllib.parse import urlparse
     
     import aiohttp
     import requests
    +from urllib3.util import parse_url
     
     from vllm.version import __version__ as VLLM_VERSION
     
    @@ -37,7 +37,7 @@ async def get_async_client(self) -> aiohttp.ClientSession:
             return self._async_client
     
         def _validate_http_url(self, url: str):
    -        parsed_url = urlparse(url)
    +        parsed_url = parse_url(url)
     
             if parsed_url.scheme not in ("http", "https"):
                 raise ValueError(
    
  • vllm/envs.py+2 2 modified
    @@ -442,9 +442,9 @@ def get_vllm_port() -> int | None:
         try:
             return int(port)
         except ValueError as err:
    -        from urllib.parse import urlparse
    +        from urllib3.util import parse_url
     
    -        parsed = urlparse(port)
    +        parsed = parse_url(port)
             if parsed.scheme:
                 raise ValueError(
                     f"VLLM_PORT '{port}' appears to be a URI. "
    
  • vllm/multimodal/utils.py+15 10 modified
    @@ -9,13 +9,13 @@
     from itertools import groupby
     from pathlib import Path
     from typing import TYPE_CHECKING, Any, TypeVar
    -from urllib.parse import ParseResult, urlparse
     from urllib.request import url2pathname
     
     import numpy as np
     import numpy.typing as npt
     import torch
     from PIL import Image, UnidentifiedImageError
    +from urllib3.util import Url, parse_url
     
     import vllm.envs as envs
     from vllm.connections import HTTPConnection, global_http_connection
    @@ -101,11 +101,14 @@ def __init__(
     
         def _load_data_url(
             self,
    -        url_spec: ParseResult,
    +        url_spec: Url,
             media_io: MediaIO[_M],
         ) -> _M:  # type: ignore[type-var]
    -        data_spec, data = url_spec.path.split(",", 1)
    +        url_spec_path = url_spec.path or ""
    +        data_spec, data = url_spec_path.split(",", 1)
             media_type, data_type = data_spec.split(";", 1)
    +        # media_type starts with a leading "/" (e.g., "/video/jpeg")
    +        media_type = media_type.lstrip("/")
     
             if data_type != "base64":
                 msg = "Only base64 data URLs are supported for now."
    @@ -115,7 +118,7 @@ def _load_data_url(
     
         def _load_file_url(
             self,
    -        url_spec: ParseResult,
    +        url_spec: Url,
             media_io: MediaIO[_M],
         ) -> _M:  # type: ignore[type-var]
             allowed_local_media_path = self.allowed_local_media_path
    @@ -124,7 +127,9 @@ def _load_file_url(
                     "Cannot load local files without `--allowed-local-media-path`."
                 )
     
    -        filepath = Path(url2pathname(url_spec.netloc + url_spec.path))
    +        url_spec_path = url_spec.path or ""
    +        url_spec_netloc = url_spec.netloc or ""
    +        filepath = Path(url2pathname(url_spec_netloc + url_spec_path))
             if allowed_local_media_path not in filepath.resolve().parents:
                 raise ValueError(
                     f"The file path {filepath} must be a subpath "
    @@ -133,7 +138,7 @@ def _load_file_url(
     
             return media_io.load_file(filepath)
     
    -    def _assert_url_in_allowed_media_domains(self, url_spec: ParseResult) -> None:
    +    def _assert_url_in_allowed_media_domains(self, url_spec: Url) -> None:
             if (
                 self.allowed_media_domains
                 and url_spec.hostname not in self.allowed_media_domains
    @@ -151,9 +156,9 @@ def load_from_url(
             *,
             fetch_timeout: int | None = None,
         ) -> _M:  # type: ignore[type-var]
    -        url_spec = urlparse(url)
    +        url_spec = parse_url(url)
     
    -        if url_spec.scheme.startswith("http"):
    +        if url_spec.scheme and url_spec.scheme.startswith("http"):
                 self._assert_url_in_allowed_media_domains(url_spec)
     
                 connection = self.connection
    @@ -181,10 +186,10 @@ async def load_from_url_async(
             *,
             fetch_timeout: int | None = None,
         ) -> _M:
    -        url_spec = urlparse(url)
    +        url_spec = parse_url(url)
             loop = asyncio.get_running_loop()
     
    -        if url_spec.scheme.startswith("http"):
    +        if url_spec.scheme and url_spec.scheme.startswith("http"):
                 self._assert_url_in_allowed_media_domains(url_spec)
     
                 connection = self.connection
    
  • vllm/utils/network_utils.py+4 2 modified
    @@ -11,12 +11,12 @@
         Sequence,
     )
     from typing import Any
    -from urllib.parse import urlparse
     from uuid import uuid4
     
     import psutil
     import zmq
     import zmq.asyncio
    +from urllib3.util import parse_url
     
     import vllm.envs as envs
     from vllm.logger import init_logger
    @@ -217,13 +217,15 @@ def find_process_using_port(port: int) -> psutil.Process | None:
     
     def split_zmq_path(path: str) -> tuple[str, str, str]:
         """Split a zmq path into its parts."""
    -    parsed = urlparse(path)
    +    parsed = parse_url(path)
         if not parsed.scheme:
             raise ValueError(f"Invalid zmq path: {path}")
     
         scheme = parsed.scheme
         host = parsed.hostname or ""
         port = str(parsed.port or "")
    +    if host.startswith("[") and host.endswith("]"):
    +        host = host[1:-1]  # Remove brackets for IPv6 address
     
         if scheme == "tcp" and not all((host, port)):
             # The host and port fields are required for tcp
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.