diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 1ab18fe1c7..903b3effcc 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -260,6 +260,15 @@ ENABLE_EASTER_EGGS = os.environ.get('ENABLE_EASTER_EGGS', 'True').lower() == 'tr # controlled origins) and fall through to the default image instead. ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.environ.get('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true' +PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset( + t.strip() + for t in os.environ.get( + 'PROFILE_IMAGE_ALLOWED_MIME_TYPES', + 'image/png,image/jpeg,image/gif,image/webp', + ).split(',') + if t.strip() +) + #################################### # WEBUI_BUILD_HASH #################################### diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 7fe5fcd2dc..33d1cd425c 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -29,7 +29,7 @@ from open_webui.models.users import ( ) from open_webui.constants import ERROR_MESSAGES -from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, STATIC_DIR +from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES, STATIC_DIR from open_webui.internal.db import get_async_session @@ -494,12 +494,18 @@ async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_u header, base64_data = user.profile_image_url.split(',', 1) image_data = base64.b64decode(base64_data) image_buffer = io.BytesIO(image_data) - media_type = header.split(';')[0].lstrip('data:') + media_type = header.split(';')[0].lstrip('data:').lower() + + if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES: + return FileResponse(f'{STATIC_DIR}/user.png') return StreamingResponse( image_buffer, media_type=media_type, - headers={'Content-Disposition': 'inline'}, + headers={ + 'Content-Disposition': 'inline', + 'X-Content-Type-Options': 'nosniff', + }, ) except Exception as e: pass diff --git a/backend/open_webui/utils/validate.py b/backend/open_webui/utils/validate.py index 1e98b41105..68a56dfadc 100644 --- a/backend/open_webui/utils/validate.py +++ b/backend/open_webui/utils/validate.py @@ -3,17 +3,13 @@ import re from urllib.parse import urlparse -# Matches the OWUI-generated profile image route. ``[^/?#]+`` accepts -# any user-ID without allowing path-traversal or query/fragment injection, -# and the ``$`` anchor rejects trailing path components. +from open_webui.env import PROFILE_IMAGE_ALLOWED_MIME_TYPES + _USER_PROFILE_IMAGE_RE = re.compile(r'^/api/v1/users/[^/?#]+/profile/image$') -# Validates MIME type and structure of base64 data URIs. Only the prefix -# is checked — validating the full base64 payload would mean running a -# regex across megabytes of data on every Pydantic instantiation for zero -# security benefit (corrupt base64 simply renders a broken image, same as -# a 404 URL). SVG is intentionally excluded: it can carry embedded scripts. -_SAFE_DATA_URI_RE = re.compile(r'^data:image/(png|jpeg|gif|webp);base64,', re.IGNORECASE) +# Data-URI prefix validator derived from PROFILE_IMAGE_ALLOWED_MIME_TYPES. +_mime_suffixes = '|'.join(re.escape(t.split('/')[-1]) for t in sorted(PROFILE_IMAGE_ALLOWED_MIME_TYPES)) +_SAFE_DATA_URI_RE = re.compile(rf'^data:image/({_mime_suffixes});base64,', re.IGNORECASE) # Exact relative paths accepted as profile images. These are the only # static-asset paths OWUI itself assigns; no prefix/wildcard matching is