fix: send image backend headers only to that backend's own origin

When an OpenAI-compatible image backend answers with a URL instead of base64, Open WebUI fetches that URL with its own Authorization header attached. Backends commonly return a presigned S3, R2 or Azure blob link on a third-party host, and S3-compatible stores reject a request carrying both an Authorization header and signed query parameters, so the download fails with a 400 and the user sees "Failed to retrieve image data from the generation backend" after a generation or edit that actually succeeded. The API key, and with ENABLE_FORWARD_USER_INFO_HEADERS the user's name, email and id, are disclosed to that host as well.

Send the headers only when the returned URL is on the same origin as the configured API base URL. This stays separate from get_image_data's trusted_base_url, which decides whether to skip SSRF validation for an admin-configured backend; the OpenAI paths keep that validation on.

Known behaviour change: a backend that serves its API and its images from different hosts behind the same bearer token now fetches those images unauthenticated.

Refs #29220
This commit is contained in:
Classic298 2026-09-05 12:48:16 +02:00
parent 85b11a4f35
commit 4a49bab834

View file

@ -477,6 +477,14 @@ def _is_same_origin(url: str, base_url: str) -> bool:
)
def _headers_for_url(headers: dict[str, str], url: str, base_url: str) -> dict[str, str] | None:
"""Send the configured backend's headers only to URLs on its own origin."""
if not _is_same_origin(url, base_url):
log.debug('Withholding headers for cross-origin image host: %s', urlparse(url).hostname)
return None
return {k: v for k, v in headers.items() if k != 'Content-Type'}
async def get_image_data(data: str, headers=None, trusted_base_url: str | None = None):
try:
if data.startswith('http://') or data.startswith('https://'):
@ -668,7 +676,7 @@ async def image_generations(
if image_url := image.get('url', None):
image_data, content_type = await get_image_data(
image_url,
{k: v for k, v in headers.items() if k != 'Content-Type'},
_headers_for_url(headers, image_url, image_config.IMAGES_OPENAI_API_BASE_URL),
)
else:
image_data, content_type = await get_image_data(image['b64_json'])
@ -1047,7 +1055,7 @@ async def image_edits(
if image_url := image.get('url', None):
image_data, content_type = await get_image_data(
image_url,
{k: v for k, v in headers.items() if k != 'Content-Type'},
_headers_for_url(headers, image_url, image_config.IMAGES_EDIT_OPENAI_API_BASE_URL),
)
else:
image_data, content_type = await get_image_data(image['b64_json'])