PT-2026-59358 · Pypi · Open-Webui

Published

2026-07-13

·

Updated

2026-07-13

CVSS v3.1

8.5

High

VectorAV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

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.
QQ20260322-202854-22-1
The current checking logic uses urlparse to parse the hostname part of the URL for verification.
QQ20260322-203014-22-2
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.
QQ20260322-203122-22-3
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.
QQ20260322-203503-22-4
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.
QQ20260322-203750-22-5

PoC

http://127.0.0.1:6666@baidu.com

Impact

SSRF

Fix

Found an issue in the description? Have something to add? Feel free to write us 👾

Related Identifiers

PYSEC-2026-2715

Affected Products

Open-Webui