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>
get_prompts_by_user_id used to fetch every active prompt (with users +
all access grants), then call AccessGrants.has_access() once per prompt
that the user did not own. With 600+ prompts this issued ~600 extra
round-trips per request and explained the multi-second delay reported in
the GET /api/v1/prompts and /api/v1/prompts/tags endpoints for non-admin
users.
Push the access check into a single SQL query via the existing
AccessGrants.has_permission_filter (EXISTS subquery), so only accessible
rows come back from the DB. Users and access grants for the surviving
rows are still batch-fetched, no N+1 anywhere on this path.
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