CVE-2026-24779

Schrödinger's URL: Breaking vLLM with Parser Differentials (CVE-2026-24779)

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 28, 2026·7 min read·7 visits

Executive Summary (TL;DR)

vLLM used two different libraries to parse URLs: one to check if it's safe, and another to actually fetch it. Attackers can exploit this disagreement to slip past the bouncer and force the AI server to access internal network resources, including cloud metadata services.

A critical Server-Side Request Forgery (SSRF) vulnerability exists in vLLM versions prior to 0.14.1. The flaw stems from a 'Parser Differential' where the validation logic (using Python's `urllib`) and the execution logic (using `urllib3`/`requests`) interpret the same URL string differently. This discrepancy allows attackers to bypass domain allowlists by crafting URLs that appear innocent to the validator but resolve to internal network targets during execution. In containerized AI environments, this exposes sensitive metadata, internal APIs, and potentially allows for Denial of Service.

The Hook: When AI Gets Curious

vLLM is the darling of the open-source AI community. It's the high-throughput, memory-efficient engine that powers a significant chunk of the Large Language Model (LLM) serving infrastructure out there. If you're chatting with a hosted Llama 3 or Mixtral model, there's a good chance vLLM is handling the inference. Recently, vLLM added 'multimodal' support—the ability to process images and audio alongside text. To make this work, the engine needs to fetch media files from external URLs.

Here is where things get spicy. Whenever a server accepts a URL from a user and promises to fetch it, a security researcher somewhere gets their wings. It is the classic setup for Server-Side Request Forgery (SSRF). The developers knew this, of course. They implemented a MediaConnector class with a strict allowlist mechanism to ensure the server only talks to trusted domains. They locked the front door.

Unfortunately, they fell victim to one of the oldest tricks in the web security book: The Parser Differential. It turns out, locking the front door doesn't help much if the lock mechanism and the door handle disagree on what a 'door' actually is. This vulnerability isn't just a coding error; it's a fundamental misunderstanding of how complex URL standards (and their implementations) can be weaponized.

The Flaw: A Tale of Two Parsers

The root cause of CVE-2026-24779 is a discrepancy between how two different Python libraries interpret a URL. This is a classic 'Time of Check to Time of Use' (TOCTOU) bug, but logic-based rather than race-condition based. The application uses urllib.parse to validate the URL, and then uses the requests library (which relies on urllib3) to fetch the URL.

The problem? urllib adheres strictly to older RFCs, while urllib3 tries to be helpful and modern, often mimicking browser behavior. Specifically, they disagree on how to handle the backslash (\) character in the authority section of a URL.

When urllib.parse sees a backslash, it often treats it as a valid character within a path or authority segment, not necessarily as a delimiter. However, urllib3 (and the underlying HTTP client) is often more aggressive about normalization. It might treat \ as a separator or, more critically, it might parse the 'user info' section of a URL differently. If you can trick the validator into thinking the hostname is trusted.com while the fetcher thinks the hostname is localhost, you win. You have successfully created a Schrödinger's URL—it is both safe and malicious effectively at the same time, depending on who is looking at it.

The Code: The Smoking Gun

Let's look at the crime scene in vllm/multimodal/utils.py. The developers wrote code that looks perfectly reasonable at first glance. They parse the URL, check the domain, and then fetch the data.

# THE VULNERABLE CODE PATTERN
 
# 1. Validation Phase (The Bouncer)
from urllib.parse import urlparse
 
url_spec = urlparse(url)
# Checks if url_spec.hostname is in the allowlist
self._assert_url_in_allowed_media_domains(url_spec)
 
# 2. Execution Phase (The Bartender)
connection = self.connection
# 'requests' library parses the raw 'url' string AGAIN internally
data = connection.get_bytes(
    url, 
    timeout=fetch_timeout
)

The fatal flaw is passing the original raw string url to connection.get_bytes. The validation was performed on url_spec, but the action is performed on url. Because requests does its own parsing internally, any difference in interpretation allows the exploit to slip through.

The fix, applied in version 0.14.1, is elegant in its simplicity: Use the same parser for everything.

- from urllib.parse import ParseResult, urlparse
+ from urllib3.util import Url, parse_url
 
  def load_from_url(self, url: str, ...) -> _M:
-     url_spec = urlparse(url)
+     url_spec = parse_url(url)
 
      if url_spec.scheme.startswith("http"):
          self._assert_url_in_allowed_media_domains(url_spec)

By switching to urllib3.util.parse_url, the validation logic now sees exactly what the fetching logic sees. If urllib3 thinks the host is malicious, the validator will now see that malicious host and block it.

The Exploit: Bypassing the Whitelist

So, how do we weaponize this? We need a URL that urllib reads as allowed-domain.com but urllib3 reads as internal-target. We can achieve this using the @ symbol (which denotes user credentials in a URL) and the backslash \.

The Payload: http://allowed-domain.com\@malicious-internal-host.local

Here is the step-by-step breakdown of the heist:

  1. Validation (urllib): The parser scans the string. It sees allowed-domain.com at the start. It doesn't treat the backslash \ as a special delimiter in this context, nor does it aggressively handle the @ if it thinks it's part of the path or authority confusion. It effectively concludes: "The hostname is allowed-domain.com." Since this is on the whitelist, the bouncer opens the velvet rope.

  2. Execution (requests/urllib3): The requests library gets the raw string. Its parser sees the @ symbol and says, "Ah, everything before this is a username!" It interprets allowed-domain.com\ as the username for authentication. It then looks at what follows for the actual hostname: malicious-internal-host.local.

  3. The Strike: vLLM initiates an HTTP GET request to malicious-internal-host.local. The allowed-domain.com part is sent merely as a Basic Auth header (or ignored), completely irrelevant to the routing of the packet. The server has been tricked into contacting an internal resource.

The Impact: Why This Matters

In a vacuum, SSRF is bad. In the context of modern AI infrastructure, it is catastrophic. vLLM is rarely deployed on a standalone laptop; it lives in massive Kubernetes clusters, often with elevated privileges or access to sensitive data planes.

By exploiting this SSRF, an attacker can:

  • Cloud Metadata Extraction: Access http://169.254.169.254 to steal AWS IAM credentials or instance identity tokens. This is the 'Game Over' scenario for cloud deployments.
  • Internal Reconnaissance: Scan the internal network (Pod-to-Pod communication) to identify other vulnerable services, databases, or management APIs that aren't exposed to the public internet.
  • DoS and Metric Poisoning: Interact with internal monitoring endpoints (like Prometheus exporters) or confuse the orchestration layer (like the llm-d component mentioned in reports) by feeding it garbage data or crashing the service.

This isn't just about reading a file; it's about pivoting from a public-facing inference endpoint to the soft, chewy center of the internal network.

Mitigation: Stopping the Bleeding

If you are running vLLM older than 0.14.1, stop reading and update. Now. The patch is the only surefire way to align the parsers.

However, we must be cynical. Patches fix bugs, but architecture fixes classes of bugs. Relying solely on a regex or a URL parser to secure your internal network is a losing strategy. Here is how you actually secure this:

  1. Network Policies: Your AI inference pods should not have outbound internet access unless strictly necessary. If they do need it, use a whitelist proxy or Kubernetes NetworkPolicies to deny access to private IP ranges (RFC 1918) and, crucially, the cloud metadata IP (169.254.169.254).
  2. Disable Redirects: Even with the parser fixed, Open Redirect vulnerabilities on whitelisted domains could still lead to SSRF. Set VLLM_MEDIA_URL_ALLOW_REDIRECTS=False in your environment variables. Never trust a 302 Found.
  3. DNS Resolution: A sophisticated attacker might use DNS rebinding (resolving a whitelisted domain to a private IP after the check). A robust defense involves resolving the DNS first, checking the IP against a blocklist, and then fetching via that specific IP.

Fix Analysis (1)

Technical Appendix

CVSS Score
7.1/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:L
EPSS Probability
0.04%
Top 89% most exploited

Affected Systems

vLLM inference server (versions < 0.14.1)Applications using vLLM for multimodal generationKubernetes clusters hosting vulnerable vLLM pods

Affected Versions Detail

Product
Affected Versions
Fixed Version
vLLM
vllm-project
< 0.14.10.14.1
AttributeDetail
CWE IDCWE-918
Attack VectorNetwork (AV:N)
CVSS Score7.1 (High)
Exploit StatusPoC Available
ImpactInformation Disclosure / DoS
Key FixStandardize URL parsing using urllib3
CWE-918
Server-Side Request Forgery (SSRF)

Server-Side Request Forgery (SSRF) occurs when a web application is induced to make a request to an unintended location.

Vulnerability Timeline

Fix committed to vLLM repository
2026-01-22
Public advisory and CVE published
2026-01-27

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.