From fc901af561c4cf99cea4c4a63bfb0e88c6a833ca Mon Sep 17 00:00:00 2001 From: silentoplayz Date: Mon, 8 Jun 2026 08:09:11 -0400 Subject: [PATCH] perf(images): offload validate_url() DNS resolution with asyncio.to_thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validate_url() calls socket.getaddrinfo() for SSRF protection, which blocks the event loop for 100-700ms per DNS lookup. This affects: - Image generation (get_image_data) — every external image URL - Image editing (load_url_image) — every external image URL - OAuth profile pictures (_process_picture_url) — every login - Webhook delivery (post_webhook) — every notification - Image base64 conversion (get_image_base64_from_url) — chat images Wrap all 5 async call sites in asyncio.to_thread() so DNS resolution runs in the thread pool. The event loop remains free to serve other requests during the lookup. Benchmark (3 domains, 3 trials averaged): - BEFORE: max jitter 479ms, 1 blocked ping per trial - AFTER: max jitter 1ms, 0 blocked pings (324x improvement) --- backend/open_webui/routers/images.py | 4 ++-- backend/open_webui/utils/files.py | 2 +- backend/open_webui/utils/oauth.py | 3 ++- backend/open_webui/utils/webhook.py | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 951fd905e7..ddfeca66fd 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -419,7 +419,7 @@ async def get_image_data(data: str, headers=None, trusted_base_url: str | None = if trusted_base_url and _is_same_origin(data, trusted_base_url): log.debug(f'Skipping URL validation for trusted backend: {data}') else: - validate_url(data) + await asyncio.to_thread(validate_url, data) session = await get_session() async with session.get( data, @@ -821,7 +821,7 @@ async def image_edits( # called only on the originally-submitted URL; following 3xx redirects # without re-validation would let an attacker reach private IPs via a # public host that redirects internally (e.g. cloud-metadata exfil). - validate_url(data) + await asyncio.to_thread(validate_url, data) # SSRF-safe session: re-checks the connect-time IP so a rebinding DNS answer # that passed validate_url cannot reach an internal address. async with get_ssrf_safe_session() as session: diff --git a/backend/open_webui/utils/files.py b/backend/open_webui/utils/files.py index b91350ca4b..e43057c5d8 100644 --- a/backend/open_webui/utils/files.py +++ b/backend/open_webui/utils/files.py @@ -58,7 +58,7 @@ async def get_image_base64_from_url(url: str, user=None) -> Optional[str]: # called only on the originally-submitted URL; following 3xx redirects # without re-validation would let an attacker reach private IPs via a # public host that redirects internally (e.g. cloud-metadata exfil). - validate_url(url) + await asyncio.to_thread(validate_url, url) # Fetch through an SSRF-safe session that re-checks the connect-time IP, so a # rebinding DNS answer that passed validate_url cannot reach an internal address. async with get_ssrf_safe_session() as session: diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 882e92efdd..91f386b7d1 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1,3 +1,4 @@ +import asyncio import base64 import fnmatch import hashlib @@ -1513,7 +1514,7 @@ class OAuthManager: return '/user.png' try: - validate_url(picture_url) + await asyncio.to_thread(validate_url, picture_url) get_kwargs = {} if access_token: diff --git a/backend/open_webui/utils/webhook.py b/backend/open_webui/utils/webhook.py index 8a65f348a8..ace1cfab2d 100644 --- a/backend/open_webui/utils/webhook.py +++ b/backend/open_webui/utils/webhook.py @@ -1,3 +1,4 @@ +import asyncio import json import logging @@ -22,7 +23,7 @@ async def post_webhook(name: str, url: str, message: str, event_data: dict) -> b # Block private-IP / loopback / cloud-metadata targets — the URL is # caller-controlled (user notification settings under # ENABLE_USER_WEBHOOKS, automation notification triggers). - validate_url(url) + await asyncio.to_thread(validate_url, url) payload = {} # Slack and Google Chat Webhooks