From d11e06f1b7f6f6298c31432d88fec7c4a40c7499 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 18:09:15 +0200 Subject: [PATCH] fix: prevent redirect-based SSRF and enforce collecton write access (#24524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: prevent redirect-based SSRF in get_image_base64_from_url Cohort follow-up to PR #24491. That PR patched three call sites (SafeWebBaseLoader._scrape, get_content_from_url, load_url_image) to pass allow_redirects=False on the underlying HTTP client; this fourth call site in utils/files.py was missed. get_image_base64_from_url() is invoked from convert_url_images_to_base64 in utils/middleware.py on every /api/chat/completions request whose message content includes an image_url part. validate_url() is called on the originally-submitted URL only; the aiohttp session.get() call had no allow_redirects argument and the shared session pool does not override the aiohttp default (allow_redirects=True). An authenticated user sending a chat message with image_url pointing at an attacker host that 302-redirects to 169.254.169.254 / 127.0.0.1 / RFC1918 reached the internal target. This is the most reachable variant in the redirect cluster: no special endpoint, no admin permission, no feature flag. Apply the same one-line fix as the other three call sites: pass allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS (defaults to False). Reported by nayakchinmohan in GHSA-88jq-grjp-jx6f; consolidated under GHSA-rh5x-h6pp-cjj6. Co-authored-by: nayakchinmohan * fix: enforce collection write access on process_file endpoint Cohort follow-up to ba83613ff. That commit added _validate_collection_access to process_text and process_web (the user-supplied collection_name path) but missed process_file in the same router. process_file accepts a user-supplied collection_name and writes the file's embedded content into that collection via save_docs_to_vector_db. The file_id is gated by file ownership (line 1562) but collection_name was unchecked, so an authenticated user could append content from a file they own into another user's knowledge-base collection by passing the victim's KB UUID as collection_name. Identical pattern to the process_text and process_web gaps that ba83613ff closed. Apply the same one-line gate as the sibling endpoints: when collection_name is user-supplied (not the default file-{file.id} fallback), require write access via _validate_collection_access. The shared validator delegates to filter_accessible_collections, which already correctly handles file-* prefixes (via has_access_to_file) and KB UUIDs (via Knowledges.check_access_by_user_id) — admins bypass. Reported by tenbbughunters (Tenable) in GHSA-4g37-7p2c-38r9 (the comprehensive write-path filing covering process_text / process_file / process_web / process_youtube and the _validate_collection_access UUID root cause), and independently re-identified for the missed process_file call site by kodareef5 in GHSA-4m74-3cmc-293g. Co-authored-by: tenbbughunters Co-authored-by: kodareef5 * fix: enforce collection write access on process_files_batch endpoint Cohort follow-up to ba83613ff and the prior process_file fix on this branch. process_files_batch (line 2604) is the third write endpoint in the same router that accepts a user-supplied collection_name; it was covered in the same Tenable filing as process_file and was missed by the same cohort fix. The endpoint validates per-file ownership at line 2642 but does not check whether the caller has write access to the target collection_name before save_docs_to_vector_db writes into it at line 2683-2690 with add=True. Apply the same one-line gate as the sibling endpoints. Validate only when collection_name is user-supplied (truthy) so the existing fall through behavior for the None case is unchanged. Same Tenable / kodareef5 cohort as the previous commit. Co-authored-by: tenbbughunters Co-authored-by: kodareef5 --------- Co-authored-by: nayakchinmohan Co-authored-by: tenbbughunters Co-authored-by: kodareef5 --- backend/open_webui/routers/retrieval.py | 5 +++++ backend/open_webui/utils/files.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index dfd503f035..201e6a63fb 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1567,6 +1567,8 @@ async def process_file( if collection_name is None: collection_name = f'file-{file.id}' + else: + await _validate_collection_access([collection_name], user, access_type='write') if form_data.content: # Update the content in the file @@ -2617,6 +2619,9 @@ async def process_files_batch( collection_name = form_data.collection_name + if collection_name: + await _validate_collection_access([collection_name], user, access_type='write') + file_results: List[BatchProcessFilesResult] = [] file_errors: List[BatchProcessFilesResult] = [] file_updates: List[FileUpdateForm] = [] diff --git a/backend/open_webui/utils/files.py b/backend/open_webui/utils/files.py index 8149987fe4..6b821d58b6 100644 --- a/backend/open_webui/utils/files.py +++ b/backend/open_webui/utils/files.py @@ -26,7 +26,11 @@ import base64 import io import re -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK +from open_webui.env import ( + AIOHTTP_CLIENT_ALLOW_REDIRECTS, + AIOHTTP_CLIENT_SESSION_SSL, + ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK, +) from open_webui.utils.session_pool import get_session BASE64_IMAGE_URL_PREFIX = re.compile(r'data:image/\w+;base64,', re.IGNORECASE) @@ -53,11 +57,17 @@ _IMAGE_MIME_FALLBACK = { async def get_image_base64_from_url(url: str) -> Optional[str]: try: if url.startswith('http'): - # Validate URL to prevent SSRF attacks against local/private networks + # Validate URL to prevent SSRF attacks against local/private networks. + # allow_redirects=False prevents redirect-based SSRF: validate_url() is + # 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) # Download the image from the URL session = await get_session() - async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as response: + async with session.get( + url, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS + ) as response: response.raise_for_status() image_data = await response.read() encoded_string = base64.b64encode(image_data).decode('utf-8')