diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 5372aa0ce3..21f26ad549 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -780,6 +780,13 @@ PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset( if t.strip() ) +# Max stored length (bytes) of a data:image profile URI; bounds Postgres/Redis +# bloat from inline avatars and model icons. Unset (default) disables the cap. +_profile_image_max_data_uri_size = os.getenv('PROFILE_IMAGE_MAX_DATA_URI_SIZE', '').strip() +PROFILE_IMAGE_MAX_DATA_URI_SIZE = ( + int(_profile_image_max_data_uri_size) if _profile_image_max_data_uri_size else None +) + #################################### # Forward Headers #################################### diff --git a/backend/open_webui/utils/validate.py b/backend/open_webui/utils/validate.py index 68a56dfadc..27fd56ce98 100644 --- a/backend/open_webui/utils/validate.py +++ b/backend/open_webui/utils/validate.py @@ -3,7 +3,10 @@ import re from urllib.parse import urlparse -from open_webui.env import PROFILE_IMAGE_ALLOWED_MIME_TYPES +from open_webui.env import ( + PROFILE_IMAGE_ALLOWED_MIME_TYPES, + PROFILE_IMAGE_MAX_DATA_URI_SIZE, +) _USER_PROFILE_IMAGE_RE = re.compile(r'^/api/v1/users/[^/?#]+/profile/image$') @@ -40,6 +43,7 @@ def validate_profile_image_url(url: str) -> str: - SVG data URIs (can contain embedded scripts) - Arbitrary relative paths (prevents authenticated GET triggers) - Scheme-relative URLs (``//host/path``) + - data URIs larger than PROFILE_IMAGE_MAX_DATA_URI_SIZE bytes """ if not url: return url @@ -70,6 +74,11 @@ def validate_profile_image_url(url: str) -> str: # The regex enforces the ;base64, boundary and is case-insensitive # per the data-URI / MIME-type specs. if _SAFE_DATA_URI_RE.match(url): + if PROFILE_IMAGE_MAX_DATA_URI_SIZE and len(url) > PROFILE_IMAGE_MAX_DATA_URI_SIZE: + raise ValueError( + f'Invalid profile image URL: data URI exceeds the ' + f'{PROFILE_IMAGE_MAX_DATA_URI_SIZE}-byte limit.' + ) return url raise ValueError(