VYPR
Moderate severityNVD Advisory· Published Mar 9, 2026· Updated Mar 10, 2026

SSRF Protection Bypass in vLLM

CVE-2026-25960

Description

vLLM is an inference and serving engine for large language models (LLMs). The SSRF protection fix for CVE-2026-24779 add in 0.15.1 can be bypassed in the load_from_url_async method due to inconsistent URL parsing behavior between the validation layer and the actual HTTP client. The SSRF fix uses urllib3.util.parse_url() to validate and extract the hostname from user-provided URLs. However, load_from_url_async uses aiohttp for making the actual HTTP requests, and aiohttp internally uses the yarl library for URL parsing. This vulnerability in 0.17.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vllmPyPI
>= 0.15.1, < 0.17.00.17.0

Affected products

1

Patches

1
6f3b2047abd4

[Core] Fix SSRF bypass via backslash-@ URL parsing inconsistency (#34743)

https://github.com/vllm-project/vllmRussell BryantFeb 18, 2026via ghsa
2 files changed · +59 2
  • tests/multimodal/media/test_connector.py+57 0 modified
    @@ -7,8 +7,10 @@
     import os
     from tempfile import NamedTemporaryFile, TemporaryDirectory
     
    +import aiohttp
     import numpy as np
     import pytest
    +import requests
     import torch
     from PIL import Image, ImageChops
     
    @@ -318,3 +320,58 @@ async def test_allowed_media_domains(video_url: str, num_frames: int):
     
         with pytest.raises(ValueError):
             _, _ = await connector.fetch_video_async(disallowed_url)
    +
    +
    +@pytest.mark.asyncio
    +async def test_ssrf_bypass_backslash_in_url(local_asset_server):
    +    """Verify that backslash-@ URL parsing confusion cannot bypass the
    +    allowed_media_domains check (GHSA-v359-jj2v-j536).
    +
    +    urllib3.parse_url() and aiohttp/yarl disagree on how to parse a
    +    backslash before ``@``.  urllib3 treats ``\\`` as part of the path
    +    (encoding it as ``%5C``), while yarl treats it as a userinfo
    +    separator, changing the effective host.  The fix normalises the URL
    +    through urllib3 *before* handing it to aiohttp so both layers agree.
    +    """
    +    port = local_asset_server.port
    +    asset = TEST_IMAGE_ASSETS[0]
    +
    +    # Craft the bypass payload: urllib3 sees host=127.0.0.1, but an
    +    # un-patched aiohttp would see host=example.com.
    +    bypass_url = f"http://127.0.0.1:{port}\\@example.com/{asset}"
    +
    +    connector = MediaConnector(
    +        allowed_media_domains=["127.0.0.1"],
    +    )
    +
    +    # After the fix the request is made to 127.0.0.1 (the local asset
    +    # server) using the normalised URL.  The normalised path will be
    +    # /%5C@example.com/<asset> which won't match any file the server
    +    # knows about, so we expect an HTTP error — but crucially NOT a
    +    # successful fetch from example.com.
    +    with pytest.raises(requests.exceptions.HTTPError):
    +        connector.fetch_image(bypass_url)
    +
    +    with pytest.raises(aiohttp.ClientResponseError):
    +        await connector.fetch_image_async(bypass_url)
    +
    +
    +@pytest.mark.asyncio
    +async def test_ssrf_bypass_backslash_disallowed_domain():
    +    """The reverse direction: even when the *attacker-controlled* host
    +    appears in the urllib3-parsed hostname position the allowlist must
    +    still block it.
    +    """
    +    # urllib3.parse_url sees host=example.com which is NOT in the
    +    # allowlist, so this must be rejected before any request is made.
    +    bypass_url = "https://example.com\\@safe.example.org/image.png"
    +
    +    connector = MediaConnector(
    +        allowed_media_domains=["safe.example.org"],
    +    )
    +
    +    with pytest.raises(ValueError, match="allowed domains"):
    +        connector.fetch_image(bypass_url)
    +
    +    with pytest.raises(ValueError, match="allowed domains"):
    +        await connector.fetch_image_async(bypass_url)
    
  • vllm/multimodal/media/connector.py+2 2 modified
    @@ -146,7 +146,7 @@ def load_from_url(
     
                 connection = self.connection
                 data = connection.get_bytes(
    -                url,
    +                url_spec.url,
                     timeout=fetch_timeout,
                     allow_redirects=envs.VLLM_MEDIA_URL_ALLOW_REDIRECTS,
                 )
    @@ -177,7 +177,7 @@ async def load_from_url_async(
     
                 connection = self.connection
                 data = await connection.async_get_bytes(
    -                url,
    +                url_spec.url,
                     timeout=fetch_timeout,
                     allow_redirects=envs.VLLM_MEDIA_URL_ALLOW_REDIRECTS,
                 )
    

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

6

News mentions

0

No linked articles in our index yet.