mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-10 04:20:44 +02:00
perf(images): offload validate_url() DNS resolution with asyncio.to_thread (#25825)
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) Co-authored-by: Tim Baek <tim@openwebui.com>
This commit is contained in:
@@ -433,7 +433,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,
|
||||
@@ -862,7 +862,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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import fnmatch
|
||||
import hashlib
|
||||
@@ -1603,7 +1604,7 @@ class OAuthManager:
|
||||
return '/user.png'
|
||||
|
||||
try:
|
||||
validate_url(picture_url)
|
||||
await asyncio.to_thread(validate_url, picture_url)
|
||||
|
||||
get_kwargs = {}
|
||||
if access_token:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -31,10 +32,10 @@ async def post_webhook(
|
||||
) -> bool:
|
||||
try:
|
||||
log.debug(f'post_webhook: {url}, {message}, {event_data}')
|
||||
# Block private-IP / loopback / cloud-metadata targets for every
|
||||
# webhook source, including admin-managed event webhooks and
|
||||
# user-configured notification URLs.
|
||||
validate_url(url)
|
||||
# Block private-IP / loopback / cloud-metadata targets — the URL is
|
||||
# caller-controlled (user notification settings under
|
||||
# ENABLE_USER_WEBHOOKS, automation notification triggers).
|
||||
await asyncio.to_thread(validate_url, url)
|
||||
payload = {}
|
||||
|
||||
# Slack and Google Chat Webhooks
|
||||
|
||||
Reference in New Issue
Block a user