Schrödinger's URL: Breaking vLLM with Parser Differentials (CVE-2026-24779)
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:
-
Validation (urllib): The parser scans the string. It sees
allowed-domain.comat 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 isallowed-domain.com." Since this is on the whitelist, the bouncer opens the velvet rope. -
Execution (requests/urllib3): The
requestslibrary gets the raw string. Its parser sees the@symbol and says, "Ah, everything before this is a username!" It interpretsallowed-domain.com\as the username for authentication. It then looks at what follows for the actual hostname:malicious-internal-host.local. -
The Strike: vLLM initiates an HTTP GET request to
malicious-internal-host.local. Theallowed-domain.compart 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.254to 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-dcomponent 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:
- 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). - Disable Redirects: Even with the parser fixed, Open Redirect vulnerabilities on whitelisted domains could still lead to SSRF. Set
VLLM_MEDIA_URL_ALLOW_REDIRECTS=Falsein your environment variables. Never trust a 302 Found. - 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.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:LAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
vLLM vllm-project | < 0.14.1 | 0.14.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-918 |
| Attack Vector | Network (AV:N) |
| CVSS Score | 7.1 (High) |
| Exploit Status | PoC Available |
| Impact | Information Disclosure / DoS |
| Key Fix | Standardize URL parsing using urllib3 |
MITRE ATT&CK Mapping
Server-Side Request Forgery (SSRF) occurs when a web application is induced to make a request to an unintended location.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.