CVE-2026-45400
Open WebUI: Server-Side Request Forgery (SSRF) bypass in `validate_url`
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.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
8.5
Open WebUI: Server-Side Request Forgery (SSRF) bypass in `validate_url`
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.
- Record
CVE-2026-45400- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
EUVDEUVD-2026-30636
8.5
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.
- Record
EUVD-2026-30636- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-8w7q-q5jp-jvgx
8.5
Open WebUI has a 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 validate_url.
<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, validate_url 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 as1.1.1.1(public)requeststreats\as a path character, connecting to127.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 open_webui.constants.ERROR_MESSAGES.INVALID_URL
INVALID_URL = (
"Oops! The URL you provided is invalid. Please double-check and try again."
)
# Same semantics as open_webui.config (ENABLE_RAG_LOCAL_WEB_FETCH / WEB_FETCH_FILTER_LIST)
ENABLE_RAG_LOCAL_WEB_FETCH = (
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
)
_DEFAULT_WEB_FETCH_FILTER_LIST = [
"!169.254.169.254",
"!fd00:ec2::254",
"!metadata.google.internal",
"!metadata.azure.com",
"!100.100.100.200",
]
_web_fetch_filter_env = os.getenv("WEB_FETCH_FILTER_LIST", "")
if _web_fetch_filter_env == "":
_web_fetch_filter_env_list: list[str] = []
else:
_web_fetch_filter_env_list = [
item.strip()
for item in _web_fetch_filter_env.split(",")
if item.strip()
]
WEB_FETCH_FILTER_LIST = list(
set(_DEFAULT_WEB_FETCH_FILTER_LIST + _web_fetch_filter_env_list)
)
def get_allow_block_lists(filter_list):
allow_list = []
block_list = []
if filter_list:
for d in filter_list:
if d.startswith("!"):
block_list.append(d[1:].strip())
else:
allow_list.append(d.strip())
return allow_list, block_list
def is_string_allowed(
string: Union[str, Sequence[str]], filter_list: Optional[list[str]] = None
) -> bool:
if not filter_list:
return True
allow_list, block_list = get_allow_block_lists(filter_list)
strings = [string] if isinstance(string, str) else list(string)
if allow_list:
if not any(s.endswith(allowed) for s in strings for allowed in allow_list):
return False
if any(s.endswith(blocked) for s in strings for blocked in block_list):
return False
return True
def resolve_hostname(hostname):
# Get address information
addr_info = socket.getaddrinfo(hostname, None)
# Extract IP addresses from address information
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
return ipv4_addresses, ipv6_addresses
def _validators_url_accept(url: str) -> bool:
"""
Stand-in for python-validators url(): True if string looks like http(s) URL with host.
"""
try:
u = url.strip()…
- Record
GHSA-8w7q-q5jp-jvgx- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
NVD
8.5
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.
- Record
CVE-2026-45400- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSV
8.5
Open WebUI: Server-Side Request Forgery (SSRF) bypass in `validate_url`
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.
- Record
CVE-2026-45400- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVGHSA-8w7q-q5jp-jvgx
8.5
Open WebUI has a 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 validate_url.
<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, validate_url 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 as1.1.1.1(public)requeststreats\as a path character, connecting to127.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 open_webui.constants.ERROR_MESSAGES.INVALID_URL
INVALID_URL = (
"Oops! The URL you provided is invalid. Please double-check and try again."
)
# Same semantics as open_webui.config (ENABLE_RAG_LOCAL_WEB_FETCH / WEB_FETCH_FILTER_LIST)
ENABLE_RAG_LOCAL_WEB_FETCH = (
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
)
_DEFAULT_WEB_FETCH_FILTER_LIST = [
"!169.254.169.254",
"!fd00:ec2::254",
"!metadata.google.internal",
"!metadata.azure.com",
"!100.100.100.200",
]
_web_fetch_filter_env = os.getenv("WEB_FETCH_FILTER_LIST", "")
if _web_fetch_filter_env == "":
_web_fetch_filter_env_list: list[str] = []
else:
_web_fetch_filter_env_list = [
item.strip()
for item in _web_fetch_filter_env.split(",")
if item.strip()
]
WEB_FETCH_FILTER_LIST = list(
set(_DEFAULT_WEB_FETCH_FILTER_LIST + _web_fetch_filter_env_list)
)
def get_allow_block_lists(filter_list):
allow_list = []
block_list = []
if filter_list:
for d in filter_list:
if d.startswith("!"):
block_list.append(d[1:].strip())
else:
allow_list.append(d.strip())
return allow_list, block_list
def is_string_allowed(
string: Union[str, Sequence[str]], filter_list: Optional[list[str]] = None
) -> bool:
if not filter_list:
return True
allow_list, block_list = get_allow_block_lists(filter_list)
strings = [string] if isinstance(string, str) else list(string)
if allow_list:
if not any(s.endswith(allowed) for s in strings for allowed in allow_list):
return False
if any(s.endswith(blocked) for s in strings for blocked in block_list):
return False
return True
def resolve_hostname(hostname):
# Get address information
addr_info = socket.getaddrinfo(hostname, None)
# Extract IP addresses from address information
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
return ipv4_addresses, ipv6_addresses
def _validators_url_accept(url: str) -> bool:
"""
Stand-in for python-validators url(): True if string looks like http(s) URL with host.
"""
try:
u = url.strip()…
- Record
GHSA-8w7q-q5jp-jvgx- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVPYSEC-2026-2715
8.5
Open WebUI has a 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 validate_url.
<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, validate_url 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 as1.1.1.1(public)requeststreats\as a path character, connecting to127.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 open_webui.constants.ERROR_MESSAGES.INVALID_URL
INVALID_URL = (
"Oops! The URL you provided is invalid. Please double-check and try again."
)
# Same semantics as open_webui.config (ENABLE_RAG_LOCAL_WEB_FETCH / WEB_FETCH_FILTER_LIST)
ENABLE_RAG_LOCAL_WEB_FETCH = (
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
)
_DEFAULT_WEB_FETCH_FILTER_LIST = [
"!169.254.169.254",
"!fd00:ec2::254",
"!metadata.google.internal",
"!metadata.azure.com",
"!100.100.100.200",
]
_web_fetch_filter_env = os.getenv("WEB_FETCH_FILTER_LIST", "")
if _web_fetch_filter_env == "":
_web_fetch_filter_env_list: list[str] = []
else:
_web_fetch_filter_env_list = [
item.strip()
for item in _web_fetch_filter_env.split(",")
if item.strip()
]
WEB_FETCH_FILTER_LIST = list(
set(_DEFAULT_WEB_FETCH_FILTER_LIST + _web_fetch_filter_env_list)
)
def get_allow_block_lists(filter_list):
allow_list = []
block_list = []
if filter_list:
for d in filter_list:
if d.startswith("!"):
block_list.append(d[1:].strip())
else:
allow_list.append(d.strip())
return allow_list, block_list
def is_string_allowed(
string: Union[str, Sequence[str]], filter_list: Optional[list[str]] = None
) -> bool:
if not filter_list:
return True
allow_list, block_list = get_allow_block_lists(filter_list)
strings = [string] if isinstance(string, str) else list(string)
if allow_list:
if not any(s.endswith(allowed) for s in strings for allowed in allow_list):
return False
if any(s.endswith(blocked) for s in strings for blocked in block_list):
return False
return True
def resolve_hostname(hostname):
# Get address information
addr_info = socket.getaddrinfo(hostname, None)
# Extract IP addresses from address information
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
return ipv4_addresses, ipv6_addresses
def _validators_url_accept(url: str) -> bool:
"""
Stand-in for python-validators url(): True if string looks like http(s) URL with host.
"""
try:
u = url.strip()…
- Record
PYSEC-2026-2715- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
open-webuiPyPI |
through 0.9.4 | 0.9.5 |
open-webuiPyPI |
before 0.9.5, plus listed versions 0.1.1240.1.1250.2.00.2.10.2.2 Show 45 moreShow less0.2.30.2.40.2.50.3.00.3.10.3.100.3.120.3.130.3.140.3.150.3.160.3.170.3.17.dev20.3.17.dev30.3.17.dev40.3.17.dev50.3.180.3.190.3.20.3.200.3.210.3.220.3.230.3.240.3.250.3.260.3.270.3.27.dev10.3.27.dev20.3.27.dev30.3.280.3.290.3.30.3.300.3.30.dev10.3.30.dev20.3.310.3.31.dev10.3.320.3.330.3.33.dev10.3.340.3.350.3.40.3.5 |
0.9.5 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-45400 is a high vulnerability (CVSS 8.5) in Openwebui Open Webui, published May 14, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.29%, the 21st percentile. Fixed in open-webui 0.9.5.
References
github.com/open-webui/open-webui/security/advisories/GHSA-8w7q-q5jp-jvgx
github.com/open-webui/open-webui/releases/tag/v0.9.0
nvd.nist.gov/vuln/detail/CVE-2026-45400
github.com/advisories/GHSA-8w7q-q5jp-jvgx
github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45400.json
github.com/open-webui/open-webui
pypi.org/project/open-webui
Left out here: 106 versions and the rest of 3 source descriptions. The JSON record has them.