validate_url() in retrieval/web/utils.py only validates the initial URL.
The HTTP clients used downstream (sync requests, sync requests via the
parent WebBaseLoader._scrape, aiohttp via load_url_image) followed 3xx
redirects by default and did not re-validate the redirect target against
the private-IP / metadata-IP block list. An authenticated user could
submit a public URL that 302-redirected to an internal address (RFC1918,
127.0.0.1, 169.254.169.254, etc.) and the redirected response was returned
to them, enabling SSRF reads of internal services and cloud metadata.
Three call sites needed allow_redirects=False to match the policy already
enforced on the async _fetch() path:
- SafeWebBaseLoader: override requests_kwargs in __init__ so that the
inherited synchronous _scrape() path passes allow_redirects=False to
self.session.get() (the parent WebBaseLoader uses requests' default
allow_redirects=True).
- get_content_from_url (retrieval/utils.py): pass allow_redirects=False
on the streamed requests.get(...) call.
- load_url_image (routers/images.py, image-edits endpoint): pass
allow_redirects=False on the aiohttp session.get(...) call.
Reports consolidated under GHSA-rh5x-h6pp-cjj6:
- GHSA-rh5x-h6pp-cjj6 (tenbbughunters / Tenable) - sync _scrape
- GHSA-5vxg-6gmv-m2qr (YLChen-007) - load_url_image
- GHSA-hf76-c83f-63w2 (tempcollab) - aiohttp _fetch (already fixed)
- GHSA-h55f-h5fh-mvm4 (sneaXOR) - get_content_from_url
* refac(routers): reject external URLs in profile/model image handlers
* refac(ui): centralize image URL validation in safeImageUrl helper
* refac(auths): make signout POST-only
* refac: gate external profile image redirect behind ENABLE_PROFILE_IMAGE_URL_FORWARDING
Restore the 302 redirect for external http(s) profile image URLs in
the user and model profile-image endpoints, but gate it behind a new
ENABLE_PROFILE_IMAGE_URL_FORWARDING env flag (default: True).
Existing deployments that rely on external profile image forwarding
continue to work unchanged. Operators who want to suppress the
redirect (to prevent client-side IP/UA/Referer leaks) can set the
flag to False.
Non-admin GET /api/v1/prompts/tags went through get_prompts_by_user_id,
which loaded every active prompt with its full content/data/meta plus
owner records and all access grants, then ran one has_access query per
prompt that wasn't owned by the caller - all so the endpoint could
collapse the result to a sorted tag list. With 600 prompts this took
several seconds while the admin path (a single SELECT) returned in <1s.
Add Prompts.get_tags_by_user_id which selects only the tags column and
applies the same EXISTS-based access filter used by /list. Also tighten
the admin get_tags to project just the tags column instead of full rows.
The endpoint is now one DB query (plus one for groups), no row hydration,
no N+1.
Co-authored-by: Claude <noreply@anthropic.com>
Convert the /chats/all endpoint from loading all user chats into memory
at once to a streaming NDJSON response that fetches chats in batches of
100. This prevents Out-of-Memory crashes for users with large chat
histories.
Backend: Added async generator that paginates through chats with
short-lived DB sessions per batch (critical for SQLite lock release).
Frontend: Updated getAllChats to consume the NDJSON stream via
ReadableStream reader, accumulating results for the export file.
Ref: open-webui#22206
The transcription endpoint was async but called the synchronous transcribe() function directly, blocking the single-threaded uvicorn event loop for the entire duration of inference. This caused all HTTP and WebSocket connections to stall for every user on the instance during STT processing.
- Add asyncio import
- Use async UploadFile.read() instead of synchronous file.file.read()
- Offload the blocking transcribe() call via asyncio.to_thread()
Closes#24169
- Return 302 to /static/favicon.png instead of streaming the same PNG per
model id so browsers can cache one asset for default avatars.
- Validate stored /static/ paths with decode, normpath, and /static
prefix checks; invalid paths fall back to favicon.
Made-with: Cursor
* perf(channels): batch user lookup in model_response_handler thread history
The thread-history builder in model_response_handler called
Users.get_user_by_id once per thread message (deduped via an intra-loop
dict), producing N individual SELECTs for a thread of N unique authors.
Replace with a single Users.get_users_by_user_ids call that returns all
authors in one WHERE id IN (...) query, matching the batch pattern
already used elsewhere in this file (lines 739, 804, 1320).
Behavior is preserved: deleted users still resolve to None and fall
through to the existing 'Unknown' fallback via .get().
* refac(channels): rename loop vars to full words per review
Address reviewer feedback to use descriptive names `message` and `user`
instead of single-letter `m` and `u` in the batch user-lookup
comprehensions.
---------
Co-authored-by: Claude <noreply@anthropic.com>