CVE-2026-45400: open-webui: SSRF bypass via URL parser mismatch

GHSA-8w7q-q5jp-jvgx HIGH
Published May 14, 2026
CISO Take

Open-webui's URL validation uses Python's urlparse to extract and check hostnames before passing URLs to the requests library, but the two parsers disagree on how to interpret URLs containing backslash characters — a crafted URL like http://127.0.0.1:6666\@1.1.1.1 causes urlparse to extract 1.1.1.1 as the public hostname while requests actually connects to the internal 127.0.0.1:6666, fully bypassing the SSRF filter. With CVSS 8.5 (Scope:Changed, Confidentiality:High) and only low-privilege credentials required, any authenticated user can silently pivot to internal services including cloud metadata endpoints, Ollama inference APIs, or adjacent Docker-networked services. The package carries a history of 91 CVEs and a package risk score of 38/100, indicating a persistent security debt that elevates urgency beyond this single issue. Upgrade to open-webui 0.9.5 immediately; if patching is not feasible within 24 hours, set ENABLE_RAG_LOCAL_WEB_FETCH=False, add explicit egress firewall rules blocking RFC 1918 ranges from the container, and alert on inbound URL parameters containing backslash-at sequences.

Sources: GitHub Advisory NVD ATLAS

What is the risk?

HIGH. CVSS 8.5 reflects network exploitability (AV:N), low attack complexity (AC:L), minimal privilege requirement (PR:L — any authenticated user), no user interaction needed, changed scope (S:C, meaning the attacker can reach systems outside the vulnerable component's trust boundary), and high confidentiality impact. The bypass technique is trivially reproducible with a single crafted URL requiring no specialized AI or security knowledge — essentially script-kiddie level. Open-webui deployments frequently share a Docker network with Ollama inference servers, vector databases, and internal APIs, dramatically amplifying the lateral movement potential. The absence of a KEV listing and public exploit artifact does not reduce urgency: the PoC is embedded in the public advisory.

Attack Kill Chain

Craft Bypass URL
Attacker constructs a URL using the backslash-at pattern (http://internal-ip:port\@public-domain.com) to exploit the divergence between urlparse and requests hostname resolution.
AML.T0049
Bypass SSRF Validation
open-webui's validate_url passes the URL through urlparse, extracts the public domain as the hostname, resolves it to a public IP, and approves the request — unaware that requests will connect elsewhere.
Internal Service Access
The requests library interprets the backslash as a path separator and connects to the attacker-specified internal IP and port, bypassing network controls and reaching localhost or RFC 1918 services.
AML.T0053
Credential and Data Exfiltration
Attacker reads responses from internal services — cloud metadata IAM credentials, Ollama model APIs, vector database endpoints — and uses harvested secrets for lateral movement or AI asset theft.
AML.T0083

What systems are affected?

Package Ecosystem Vulnerable Range Patched
open-webui pip <= 0.9.4 0.9.5
136.3K Pushed 5d ago 75% patched ~4d to patch Full package profile →

Do you use open-webui? You're affected.

Severity & Risk

CVSS 3.1
8.5 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

Attack Surface

AV AC PR UI S C I A
AV Network
AC Low
PR Low
UI None
S Changed
C High
I Low
A None

What should I do?

1 step
  1. 1) Upgrade to open-webui 0.9.5 or later — the patch normalises URL parsing before validation. 2) If immediate patching is blocked, set ENABLE_RAG_LOCAL_WEB_FETCH=False to disable the vulnerable code path. 3) Extend WEB_FETCH_FILTER_LIST with all RFC 1918 and link-local CIDR blocks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, fd00::/8). 4) Apply Docker or Kubernetes egress network policies restricting open-webui containers from reaching internal subnets on all ports. 5) Review application and proxy logs for requests containing backslash-at patterns (\@) in URL parameters — flag and alert on any occurrence. 6) Rotate IAM credentials and API keys for services co-located in the same Docker network or cloud environment as the open-webui instance.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Article 9 - Risk management system
ISO 42001
A.6.2.3 - AI system security and resilience
NIST AI RMF
MANAGE 2.2 - Mechanisms to sustain risk management throughout the AI system lifecycle
OWASP LLM Top 10
LLM07 - Insecure Plugin Design

Frequently Asked Questions

What is CVE-2026-45400?

Open-webui's URL validation uses Python's urlparse to extract and check hostnames before passing URLs to the requests library, but the two parsers disagree on how to interpret URLs containing backslash characters — a crafted URL like http://127.0.0.1:6666\@1.1.1.1 causes urlparse to extract 1.1.1.1 as the public hostname while requests actually connects to the internal 127.0.0.1:6666, fully bypassing the SSRF filter. With CVSS 8.5 (Scope:Changed, Confidentiality:High) and only low-privilege credentials required, any authenticated user can silently pivot to internal services including cloud metadata endpoints, Ollama inference APIs, or adjacent Docker-networked services. The package carries a history of 91 CVEs and a package risk score of 38/100, indicating a persistent security debt that elevates urgency beyond this single issue. Upgrade to open-webui 0.9.5 immediately; if patching is not feasible within 24 hours, set ENABLE_RAG_LOCAL_WEB_FETCH=False, add explicit egress firewall rules blocking RFC 1918 ranges from the container, and alert on inbound URL parameters containing backslash-at sequences.

Is CVE-2026-45400 actively exploited?

No confirmed active exploitation of CVE-2026-45400 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-45400?

1) Upgrade to open-webui 0.9.5 or later — the patch normalises URL parsing before validation. 2) If immediate patching is blocked, set ENABLE_RAG_LOCAL_WEB_FETCH=False to disable the vulnerable code path. 3) Extend WEB_FETCH_FILTER_LIST with all RFC 1918 and link-local CIDR blocks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, fd00::/8). 4) Apply Docker or Kubernetes egress network policies restricting open-webui containers from reaching internal subnets on all ports. 5) Review application and proxy logs for requests containing backslash-at patterns (\@) in URL parameters — flag and alert on any occurrence. 6) Rotate IAM credentials and API keys for services co-located in the same Docker network or cloud environment as the open-webui instance.

What systems are affected by CVE-2026-45400?

This vulnerability affects the following AI/ML architecture patterns: Self-hosted LLM chat interfaces, RAG pipelines with web fetch, LLM inference backends (Ollama, local models), Containerised AI deployment environments, Cloud-hosted AI assistant platforms.

What is the CVSS score for CVE-2026-45400?

CVE-2026-45400 has a CVSS v3.1 base score of 8.5 (HIGH).

Technical Details

NVD Description

### 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

Exploitation Scenario

An attacker with a standard authenticated session in an enterprise open-webui deployment submits the URL http://169.254.169.254/latest/meta-data/iam/security-credentials/\@legitimate-site.com to the RAG web fetch feature. open-webui's validate_url calls urlparse, extracts legitimate-site.com as the hostname, resolves it to a public IP, and passes the allowlist checks. The requests library then connects to the cloud metadata endpoint at 169.254.169.254, returning temporary IAM credentials for the EC2 instance role. The attacker uses those credentials to enumerate S3 buckets containing training data and model weights, escalate to broader AWS access, and extract proprietary AI assets — all from a browser session with no elevated permissions.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Timeline

Published
May 14, 2026
Last Modified
May 14, 2026
First Seen
May 15, 2026

Related Vulnerabilities