mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-11 04:50:11 +02:00
* 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>
88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
"""Validation utilities for user-supplied input."""
|
|
|
|
import re
|
|
from urllib.parse import urlparse
|
|
|
|
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$')
|
|
|
|
# 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
|
|
# used so that arbitrary relative paths cannot trigger authenticated GETs
|
|
# against internal endpoints when rendered as ``<img>`` sources.
|
|
_SAFE_STATIC_PATHS = frozenset(
|
|
{
|
|
'/user.png',
|
|
'/favicon.png',
|
|
'/static/favicon.png',
|
|
}
|
|
)
|
|
|
|
|
|
def validate_profile_image_url(url: str) -> str:
|
|
"""
|
|
Pydantic-compatible validator for profile image URLs.
|
|
|
|
Allowed formats:
|
|
- Empty string (falls back to default avatar)
|
|
- Known static-asset paths assigned by OWUI (exact match)
|
|
- The OWUI profile-image API route ``/api/v1/users/{id}/profile/image``
|
|
- ``http://`` and ``https://`` URLs with a valid hostname
|
|
- ``data:image/{png,jpeg,gif,webp};base64,...`` URIs
|
|
|
|
Everything else is rejected, including:
|
|
- Dangerous schemes (javascript:, file:, ftp:, …)
|
|
- 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
|
|
|
|
# --- Relative paths (exact match + anchored regex only) -----------
|
|
|
|
if url in _SAFE_STATIC_PATHS:
|
|
return url
|
|
|
|
if _USER_PROFILE_IMAGE_RE.match(url):
|
|
return url
|
|
|
|
# --- Absolute URLs -------------------------------------------------
|
|
|
|
# urlparse normalises the scheme to lowercase, giving us
|
|
# case-insensitive scheme matching for free.
|
|
parsed = urlparse(url)
|
|
|
|
# External images served over HTTP(S), e.g. OAuth provider avatars.
|
|
# Require a non-empty hostname (not just netloc, which can be ":80"
|
|
# for a URL like http://:80/path with no actual host).
|
|
if parsed.scheme in ('http', 'https'):
|
|
if not parsed.hostname:
|
|
raise ValueError('Invalid profile image URL: HTTP(S) URLs must include a host.')
|
|
return url
|
|
|
|
# Base64-encoded raster images uploaded via the frontend.
|
|
# 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(
|
|
'Invalid profile image URL: must be a known internal path, '
|
|
'an HTTP(S) URL with a host, or a data:image URI (png/jpeg/gif/webp).'
|
|
)
|