- collectDirectoryFiles Firefox fallback now tracks whether the change
event has started handling files and clears the window-focus
sentinel timer as soon as it has. Previously the 500 ms timer could
fire mid-hash for a large selection and resolve the Promise early
with a partial files array — which in sync mode would misclassify
unhashed files as removed and delete them from the KB even though
the user actually selected them. Empty-picker cancellation still
resolves cleanly because changeStarted stays false in that case.
- remove_file_from_knowledge_by_id no longer drops the file DB row
when Storage.delete_file fails. A transient S3/GCS failure would
otherwise leave an orphan blob with no metadata row to retry
against. The KB association and vector entries are already gone, so
the caller sees the file removed from the KB; the 500 response
surfaces the storage failure and a later retry (manual or automated)
can complete the cleanup once storage recovers.
Resolve backend/open_webui/routers/files.py conflict in
process_uploaded_file: take upstream's async get_async_db_context()
session, which pairs with the await _process_handler(db_session) this
branch already added. The old sync 'with SessionLocal() as db_session'
block wouldn't compile against the async handler and would have
reintroduced the "coroutine created but never awaited" bug this branch
just fixed.
* fix: replace brittle profile_image_url allowlist with safe-scheme validation
The previous validation used a hardcoded allowlist of specific static
paths and a single Gravatar prefix. This rejected OWUI's own internal
API paths (e.g. /api/v1/users/{id}/profile/image) and external OAuth
avatar URLs, making it impossible to save user profiles from the admin
panel.
Replace with scheme-based validation that allows relative paths,
HTTP(S) URLs, and data:image URIs while blocking dangerous schemes
like javascript:, file:, and ftp:.
Fixes open-webui#23387
* fix: harden profile image URL validation per review feedback
- Restrict data URIs to safe raster formats (png/jpeg/gif/webp);
SVG is excluded because it can carry embedded scripts.
- Block scheme-relative URLs (//host/path) which browsers resolve
against the current protocol, bypassing the relative-path check.
* fix: use structural validation instead of prefix checks
- Use urlparse for HTTP(S) URLs: gives case-insensitive scheme
matching and rejects bare schemes with no host (e.g. https://).
- Use a compiled regex for data URIs: enforces the ;base64, boundary,
restricts to safe raster formats, and is case-insensitive per spec.
- Removes the startswith-based prefix tuple in favour of proper
URL and data URI parsing.
* fix: validate hostname not netloc, fix misleading comment
- Use parsed.hostname instead of parsed.netloc so URLs like
http://:80/path (non-empty netloc but no actual host) are rejected.
- Update data URI comment to accurately state we validate MIME type
and structure, not base64 payload integrity.
* fix: constrain relative paths to known-safe prefixes
Accepting any relative path starting with / allowed a user to set
their profile_image_url to an arbitrary internal GET endpoint. When
another user (e.g. an admin) views that profile, the browser fires
the GET with the viewer's session cookies — an authenticated GET
trigger surface.
Constrain to known-safe prefixes (/api/v1/users/, /static/) and
exact matches (/user.png, /favicon.png) which are the only relative
paths OWUI itself generates.
* fix: use exact matches and anchored regex, eliminate all prefix wildcarding
Replace all startswith-based path checks with:
- frozenset exact matches for static assets (/user.png, /favicon.png,
/static/favicon.png)
- Anchored regex for the OWUI profile image API route that accepts
only /api/v1/users/{id}/profile/image (no trailing components,
no path traversal across segments)
This eliminates every prefix-based attack surface:
- /api/v1/users/{id}/anything-else is rejected
- /static/../../etc/passwd is rejected
- /api/v1/users/../../admin/config is rejected
- Arbitrary internal GET triggers are no longer possible
* fix: exclude query/fragment delimiters from user-ID regex segment
Change [^/]+ to [^/?#]+ so that inputs like
/api/v1/users/alice?x=1/profile/image are rejected — the browser
would interpret ? as the query string start, making the actual
request target /api/v1/users/alice instead of the intended route.
* Add ownership checks to global task endpoints
- Restrict GET /api/tasks and POST /api/tasks/stop/{task_id} to admin-only
- Add new scoped POST /api/tasks/chat/{chat_id}/stop endpoint with ownership
check so regular users can stop their own chat tasks
- Allow admins to access the scoped chat task endpoints alongside owners
- Update frontend to use the new scoped stop endpoint when a chatId is available
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
* Handle temporary (local:) chat IDs in scoped task endpoints
Temporary chats use local:<socketId> as chat_id which doesn't exist in
the DB. The scoped endpoints now skip ownership checks for local: IDs
(they aren't enumerable) and use {chat_id:path} to handle the colon in
the URL path.
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
* Verify session ownership for local: chat IDs and URL-encode chat_id
- For local:<socketId> chat IDs, look up the socket's owner in
SESSION_POOL and verify it matches the requesting user (or admin)
- URL-encode chat_id in frontend fetch calls to handle special
characters (colon in local: IDs) safely
https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc
---------
Co-authored-by: Claude <noreply@anthropic.com>
- process_uploaded_file now awaits _process_handler (previously the
coroutine was created and discarded), and the inner process_file
calls inside _process_handler are awaited too. Without these awaits,
uploads with process=True / process_in_background=False returned
before processing ever ran, so data.status stayed 'pending' forever
— which is exactly why upload_and_replace_file's Step 2 check
(status == 'completed') was unreliable for valid files. Fix the
upstream root cause instead of polling for status downstream.
- Firefox directory-picker fallback in collectDirectoryFiles now
always settles its Promise: handlers are consolidated through a
single finish() helper, the modern 'cancel' event is wired up, and
a window-focus + 500ms sentinel resolves with empty files when the
picker dismisses without any event. Without this, cancelling the
picker in Firefox left the caller stuck on "Scanning directory..."
with no completion path.
- FileSyncCompareItem._validate_file_path no longer trims whitespace off
the client-provided path. This endpoint is a path-identity protocol
and some filesystems (notably Linux) treat leading/trailing spaces
as significant; stripping collapsed " report.txt" and "report.txt"
to the same key and made the compare response fail to round-trip the
client's directoryFiles lookup. Blank values (empty or whitespace-
only) are still rejected, but the returned path matches the input
character-for-character.
- syncDirectoryHandler now builds a filesByPath Map once and uses it
for O(1) lookups in the new-files and changed-files loops instead
of a per-iteration directoryFiles.find() (quadratic for large
directories).
- Both loops now have explicit else branches when a planned path
doesn't resolve to a collected file: they count it as a failure,
log the path, surface a toast, and still bump processedCount so the
summary reports real outcomes. Previously the iteration was a
silent no-op and a server-planned file that didn't resolve locally
would not even register as missed.
The previous rollback-helper extraction landed the decorator above
_rollback_new_file instead of upload_and_replace_file, which left the
POST /{id}/file/upload_and_replace route pointed at an internal helper
with the wrong signature (no id path param, no UploadFile, no
old_file_id form) and the real handler unexposed. Move the decorator
back to upload_and_replace_file and keep _rollback_new_file as a plain
private helper.
- upload_and_replace_file now compensates when Step 4 (old-file removal)
fails: the newly added file is purged from the KB collection, unlinked
from the KB, and routed through the file delete to remove storage, the
file row, and the per-file vector collection. This restores the pre-
replace state so the KB isn't left with duplicated entries drifting
from "replace" semantics. The 500 detail explicitly says the
replacement was rolled back so callers can retry cleanly.
- Directory-sync frontend now detects files that skipped browser hashing
(size > MAX_BROWSER_HASH_BYTES) and warns the user up front that those
files will be compared by size only — a content change that keeps the
same byte size will not be detected. Paths are also logged to the
console for troubleshooting.
- FileSyncCompareItem now rejects malformed payloads up front: file_path
must be non-empty, NUL-free, under 4096 chars, and must not contain
traversal segments; file_hash must be either empty (hashing skipped
signal) or a lowercase 64-char hex SHA-256; size must be >= 0. This
hardens the public API instead of relying on the frontend to behave.
- remove_file_from_knowledge_by_id with delete_file=True now hard-deletes
the file record, storage blob, and per-file vector collection only
when no other knowledge base still references that file. If another
KB still has the file attached, the request degrades to a KB-scoped
unlink so syncing KB-A can't silently wipe a file from KB-B. This
endpoint also now deletes the object-storage blob on hard-delete,
closing the previously-flagged storage leak.
- upload_and_replace_file Step 4 switches from the global file delete
route to this KB-scoped helper so the same cross-KB safeguard applies
to replace flows.
- upload_and_replace_file preserves HTTPException from upload and
remove steps instead of flattening every upstream status/detail into
a generic 400 via str(exc).
- Frontend syncDirectoryHandler remove loop switches back to
removeFileFromKnowledgeById now that the backend endpoint handles
shared files and storage cleanup safely.
- compare_files_for_sync duplicate detection uses collections.Counter
for O(n) dedup instead of list.count inside a set comprehension
(O(n^2)), which matters for large directory payloads.
- Empty incoming file_hash now falls through to the size-based
comparison branch instead of always being treated as changed, so the
browser can skip hashing large files without every such file being
flagged as modified.
- Browser calculateFileHash skips files above 100 MB (SubtleCrypto has
no streaming digest API) and returns an empty hash, letting the
server fall back to size comparison and avoiding tab OOM on very
large files.
New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.
- upload_and_replace_file now explicitly deletes the new file's vectors
from the KB collection in the Step 3 rollback path. process_file can
write embeddings before add_file_to_knowledge_by_id fails; without the
association the router delete can't discover those vectors, so they
would remain retrievable for a file record that no longer exists.
- syncDirectoryHandler switches the removed-files loop from
removeFileFromKnowledgeById (leaves storage blobs behind) to
deleteFileById, which also clears the object-storage blob and the
per-file vector collection. Each delete is guarded with try/catch and
a truthy-response check so silent null returns are counted as
failures instead of inflating the "removed" tally.
- Sync summary now reports the succeeded-removed count and rolls
removedFailed into totalFailed so the user sees real outcomes.
- compare_files_for_sync rejects payloads with duplicate file_path
entries up front, so the server no longer relies on the frontend to
dedupe — duplicates would otherwise schedule the same existing file
for replacement or removal twice.
- upload_and_replace_file now routes both the old-file removal and the
failure-path cleanup through the files router delete handler, so the
object-storage blob is removed alongside the DB row, vector entries,
and per-file vector collection. The KB-scoped remove_file helper left
storage objects behind and was accumulating orphans across syncs.
- On processing-status failure (Step 2), the newly uploaded artifact is
deleted instead of being left as an orphaned file row/blob.
- uploadFileHandler and addFileHandler now return explicit
success/failure signals so the directory sync loop can track real
outcomes. The sync summary reports succeeded counts (not planned
counts) and switches to a warning toast with a failure tally when any
upload or replace fails, instead of always reporting "Sync complete".
- Replace loop wraps uploadAndReplaceFile in try/catch so a single
failed replacement no longer aborts the whole sync, and shows a per-
file error toast with the server detail.
- Drop the unused has_access imports in files.py and knowledge.py
added by this PR (AccessGrants.has_access and has_access_to_file are
the actual call sites).
- upload_and_replace_file now awaits process_file; the missing await made
it a no-op coroutine, so the KB relation was added without embeddings
ever being generated.
- Replace the truthy-data check with an explicit status == 'completed'
gate so pending/failed files can't slip through as "processed"; surface
the stored processing error when available.
- Clean up via the files router delete handler on KB-add failure so
storage objects and the per-file vector collection are removed too;
Files.delete_file_by_id only drops the DB row and would orphan them.
- compare_files_for_sync keys existing files as a list per sync_path so
duplicate uploads no longer silently overwrite each other. The first
entry is treated as the canonical replace target; extras (both for
matched and unmatched paths) are scheduled for removal.
- compareFilesForSync / uploadAndReplaceFile wrappers fall back through
err.detail / err.message / stringification so non-API failures (network
TypeError, thrown string) produce a truthy error and actually throw,
instead of returning null and letting the sync flow report success for
failed replacements.
The validators.ipv6(ip, private=True) call always returns a falsy ValidationError because validators==0.35.0 does not support the private kwarg for IPv6. This means any hostname resolving to a private IPv6 address (::1, fd00::*, ::ffff:169.254.169.254) bypasses SSRF protection entirely, circumventing the fix for CVE-2025-65958.
Replace both the IPv4 and IPv6 validators-based private checks with Python's stdlib ipaddress module using an allowlist approach (not addr.is_global). This blocks all non-globally-routable addresses — private, loopback, link-local, reserved, multicast, and unspecified — for both IPv4 and IPv6, including IPv4-mapped IPv6 addresses.
Per RFC 4513, a Simple Bind with a non-empty DN but empty password is unauthenticated simple authentication. Many LDAP servers (OpenLDAP default, some AD configs) accept these binds, allowing account takeover without valid credentials.
Rejects empty and whitespace-only passwords before attempting the LDAP bind.
The APIKeyRestrictionMiddleware only inspected the Authorization header for sk- tokens, but get_current_user also reads API keys from cookies and x-api-key headers. This allowed complete bypass of endpoint restrictions by sending the key via an alternate transport.
Moves the restriction check into get_current_user_by_api_key so it runs regardless of how the API key was delivered. Removes the now-redundant middleware.
- Remove stray closing paren in compare_files_for_sync that broke module
import and prevented the router from loading.
- upload_and_replace_file now verifies the old file belongs to the target
knowledge base (not just that it exists globally), and propagates a 500
when the post-upload removal fails so callers can reconcile instead of
silently accumulating duplicates.
- syncDirectoryHandler splits directory-picker errors (routed through
handleUploadError) from API errors (now surfaced with the server's
detail message), so compare/upload/remove failures no longer show the
misleading "Error accessing directory" toast.
Unlike all other resource routers (knowledge, models, notes, prompts, tools, skills), the channel router did not call filter_allowed_access_grants. This allowed any user to set wildcard access grants on group channels, bypassing the admin's public sharing permission framework.
Adds filter_allowed_access_grants with the sharing.public_channels permission key to both create and update endpoints, matching the pattern used by all other resource routers.
The OAuth token exchange endpoint skipped the domain allowlist check that the normal OAuth callback enforces. An attacker with a valid OAuth token from a non-allowed domain (e.g. gmail.com) could bypass the admin's domain restriction policy entirely.
Adds the same domain validation check used in the OAuth callback, denying access when the email domain is not in the allowed list.
SESSION_POOL caches user.role at connection time and never refreshes it. When an admin demotes or deletes a user, their socket sessions retain the old cached role until voluntary disconnect, allowing continued use of admin-gated socket features (ydoc editing, channel access).
Adds disconnect_user_sessions() helper that disconnects all sockets for a user ID. Called from update_user_by_id (on role change) and delete_user_by_id. The client auto-reconnects and re-authenticates with fresh DB data.
fix: add separate original_path field for directory sync
Previously, meta.name was overloaded to store the full path for directory
sync comparison (e.g., "docs/subfolder/readme.md"). This caused potential
downstream effects since meta.name is used for display and downloads.
Changes:
- Revert meta.name to store only the sanitized base filename
- Add new meta.original_path field that preserves the full upload path
- Update sync compare logic to prioritize original_path for matching
- Fallback chain for legacy files: original_path -> name -> filename
This maintains backwards compatibility with existing files while enabling
directory structure preservation for the sync feature.
fix: use size-based fallback for legacy files to prevent sync timeout
- Replace brittle __class__.__name__ check with isinstance(metadata, FormParam)
- For legacy files without stored hashes, use file size comparison instead
of downloading and hashing files on-demand during sync comparison
- This prevents HTTP timeouts for knowledge bases with 1000+ legacy files
- Remove unused get_file_hash function and calculate_sha256 import
The catch-all /{path:path} proxy forwards any request to the upstream OpenAI-compatible API with the admin's API key and no access control. This is an intentional proxy but should be opt-in.
Adds ENABLE_OPENAI_API_PASSTHROUGH env var (defaults to False). When disabled, the catch-all returns 403. No other routers (Ollama, responses) have catch-all proxies.
The GET /channels/{id}/members endpoint checked membership for group/dm channels but had no access gate for standard channels, allowing any authenticated user with channels permission to enumerate members of private standard channels by UUID.
The model name from user input was interpolated directly into Azure deployment URL paths without validation. A user could send a model name like '../../management/foo' to traverse the URL path and hit unintended Azure endpoints with the admin's API key.
Adds _sanitize_model_for_url that rejects path separators and traversal sequences, and percent-encodes the name. Applied at convert_to_azure_payload (covers chat completions + proxy) and the responses endpoint's direct URL construction.
These four endpoints checked model existence but never verified the user has read access via AccessGrants, allowing any authenticated user to use restricted models.
Uses the canonical check_model_access helper from utils.access_control.
Both LDAP and OAuth registration checked user count before insert to determine whether to assign admin role. With multiple workers, concurrent first-user registrations could each see zero users and both create admin accounts.
Applies the insert-first-check-after pattern already used by signup_handler: insert with DEFAULT_USER_ROLE, then atomically check get_num_users()==1 and promote only the sole user to admin.
is_user_channel_member and is_user_channel_manager did not filter on is_active, allowing deactivated members to retain read/write access to group channels via direct API calls.
The /responses proxy endpoint only required authentication via
get_verified_user but did not check per-model access grants. This
allowed any authenticated user to access any model through this
endpoint, bypassing the access control system.
Extract a shared check_model_access helper into utils/access_control
and replace all inline access control blocks across openai.py and
ollama.py (7 locations) with calls to this helper. This eliminates
code duplication and prevents future policy drift between endpoints.
CWE-862: Missing Authorization
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:H (6.5 Medium)