From 83890f18b936c3cf9708e533e9df4dd4990bab1b Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:57:06 +0200 Subject: [PATCH] feat: cap profile image data URI size to bound model/avatar bloat (#25476) * feat: cap profile image data URI size to bound model/avatar bloat validate_profile_image_url() validated data-URI format (MIME allowlist, SVG rejection, scheme checks) but never its length, so a valid data:image/...;base64, passed for both custom-model icons and user avatars. Large inline images bloat Postgres and the Redis MODELS hash and degrade model-list latency. Add PROFILE_IMAGE_MAX_DATA_URI_SIZE (default 256 KiB, 0 disables) and reject oversized data URIs in the shared validator, so both model meta (ModelMeta.profile_image_url) and user avatars (UpdateProfileForm) are bounded at one chokepoint. ModelMeta already clears invalid values to None on read, so existing oversized icons stop propagating into the MODELS hash on the next refresh. Fixes #25468 Co-Authored-By: Claude Opus 4.8 (1M context) * fix: default PROFILE_IMAGE_MAX_DATA_URI_SIZE to None (no cap) Per review: opt-in rather than a 256 KiB default. Unset leaves data URIs uncapped; the validator already skips the check on a falsy value. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- backend/open_webui/env.py | 7 +++++++ backend/open_webui/utils/validate.py | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) 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(