From 01198eaeefeec034ff52808a965dc27258cb2290 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:15:31 +0200 Subject: [PATCH] Close DNS-rebinding SSRF gap in get_content_from_url probe (#25775) The web-ingest probe in get_content_from_url validated the URL at resolve time (validate_url) but then fetched with a bare requests.get, which re-resolves the hostname at connect time. An attacker-controlled name server can answer with a public IP during validation and an internal IP at connect, reaching cloud metadata / loopback / internal services (blind always; binary content-types are read back to the caller). The connection-layer guard (#24759) that closes this for the SafeWebBaseLoader path was never mounted on this probe. Route the probe through the same _SSRFSafeAdapter the loader uses, so the resolution that feeds the TCP connect is re-validated against the global-IP check. Co-authored-by: Claude Opus 4.8 (1M context) --- backend/open_webui/retrieval/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index f805fb4090..0421787819 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -191,7 +191,8 @@ def _is_text_content_type(content_type: str) -> bool: async def get_content_from_url(request, url: str) -> str: - from open_webui.retrieval.web.utils import validate_url + from open_webui.retrieval.web.utils import validate_url, _SSRFSafeAdapter + loader_config = await get_loader_config() @@ -216,7 +217,11 @@ async def get_content_from_url(request, url: str) -> str: # re-validation would let an attacker reach private IPs (RFC1918, loopback, # cloud-metadata 169.254.169.254) via a public host that redirects internally. try: - response = requests.get(url, stream=True, timeout=30, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS) + # Probe through the connect-time SSRF guard; bare requests.get re-resolves (DNS-rebinding gap). + session = requests.Session() + session.mount('http://', _SSRFSafeAdapter()) + session.mount('https://', _SSRFSafeAdapter()) + response = session.get(url, stream=True, timeout=30, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS) response.raise_for_status() content_type = response.headers.get('Content-Type', '') except Exception: