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,<huge> 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Classic298 2026-06-01 18:57:06 +02:00 committed by GitHub
parent e2502ec80f
commit 83890f18b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 17 additions and 1 deletions

View file

@ -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
####################################

View file

@ -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(