A workspace model shared publicly could be used by any user even when its
base model was private. Unregistered base models (no row in the model
table) are admin-only for direct use — get_filtered_models hides them from
non-admins and check_model_access rejects them — but has_base_model_access
treated a missing row as "no ACL" and allowed the chained request through.
has_base_model_access now takes the caller's role and only allows an
unregistered base model hop for admins, so a shared preset can no longer
reach a base model the caller could not use directly. Registered base
models keep their existing grant-based enforcement.
Claude-Session: https://claude.ai/code/session_018toPfJW1hMXAhokGaL43Ep
Co-authored-by: Claude <noreply@anthropic.com>
Since the config refactor, get_web_loader dispatched on the WEB_LOADER_ENGINE module constant, which is read from the environment once at import time. The engine selected in the Admin UI is stored under web.loader.engine in the config table but was never consulted, so UI-configured loader engines (external, playwright, firecrawl, tavily, microsoft_web_iq) were silently ignored and the built-in SafeWebBaseLoader always fetched pages directly. The same applied to the per-engine settings such as the external web loader URL and API key. This breaks egress-restricted deployments that rely on an external web loader: pages are fetched directly from the container and fail with errors like "Network is unreachable" even though an external loader is configured.
Pass the DB-backed loader settings into get_web_loader from both call sites, web search in process_web_search and web fetch via get_loader, and resolve every engine setting from them, keeping the module-level env constants as the fallback for keys that were never saved. Also initialise WebLoaderClass so an unknown engine raises the intended ValueError instead of an UnboundLocalError.
Fixes#26747
Docker compose list-form environment syntax passes quotes through verbatim, so WEB_FETCH_FILTER_LIST="" reaches the backend as two literal quote characters rather than an empty string. Config parsing turned that into the filter entry '""', which has no "!" prefix and therefore landed in the allow list. A non-empty allow list requires every host to match one of its entries, and a quotes-only pattern can never match a hostname, so every fetch_url and web loader request was rejected with "URL blocked by filter list" and surfaced to the user as "The URL you provided is invalid".
get_allow_block_lists now strips surrounding quote characters from each entry and drops entries that are empty after normalisation. Quoted but otherwise valid entries such as "example.com" or !"example.com" now behave as their unquoted forms, and garbage entries no longer convert the default blocklist into a match-nothing allowlist that blocks everything.
Fixes#26908
get_accessible_folder_files is the server-side filter that reduces a folder's attached-knowledge list (and, once #26723 lands, a direct model's) to the entries the caller may read, before that list is handed to the builtin knowledge tools as `__model_knowledge__`. It validated `file` and `collection` entries but passed `note` entries through unchecked (they fell into the `else` keep-as-is branch), even though notes are a first-class attached-knowledge type that flows through this list.
No current caller is exploitable, because every note consumer (`query_knowledge_files`, `view_note`, and the legacy retrieval path) independently re-checks note access before returning content. But relying on each consumer to remember that check is exactly the fragility this helper exists to remove, and the same `_has_read_access_to_file` membership short-circuit that makes an unvalidated `file` entry dangerous would turn any future note path that trusts list membership into an IDOR. Validate notes here so the filter enforces its own contract instead of leaning on downstream re-checks.
A note entry is now kept only when the caller owns it or holds a read grant. Notes are private by default and carry no self-grant, so ownership is checked explicitly alongside the grant lookup. Admins still bypass all checks and genuinely unknown types are still kept as-is.
Related: #26723
Since v0.10.0 a global fetch interceptor redirects to /auth and clears
localStorage.token whenever an authenticated backend request returns 401.
The OAuth callback cookie ("token", set with httponly=False so the
frontend can read it) is left behind. The auth page's oauthCallbackHandler
then immediately signs the user back in from that cookie and navigates to
"/", where the next 401 triggers the redirect again. The result is an
endless /auth and / ping-pong that renders as uncontrollable screen
flashing, and as a PWA stuck on a flashing splash screen when SvelteKit's
update check turns each navigation into a full page reload. Affected
users could only recover by clearing cookies, which matches the reports.
Clear the token cookie together with localStorage when redirecting, so
/auth stays on the login form and the user can sign in again normally.
Fixes#26731
seed_defaults inserted a row for every key in DEFAULT_CONFIG regardless
of whether the DB is authoritative for it. With ENABLE_OAUTH_PERSISTENT_CONFIG
off, the oauth.* keys were seeded from the then-current (often empty) env
values. Enabling the flag later made those stale rows override live env vars
(e.g. ENABLE_OAUTH_SIGNUP=true stopped taking effect) and further env changes
were never picked up.
Skip keys where persistent_enabled_for() is false, matching the masking the
read paths (get/get_many/get_namespace/get_all) already apply.
Claude-Session: https://claude.ai/code/session_01Vr2RCYUTXCtgtV4WMUCK86
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The VoiceRecording component stays mounted (hidden) between recordings, and the web speech engine accumulated every session's transcript into the never-reset transcription variable. Each new recording therefore confirmed all previous utterances again, so the inserted text repeated once per session and previously deleted text reappeared in the input.
Additionally, cancelling a recording (X button, Escape or a recognition error) called stopRecording(), which stops the SpeechRecognition instance and fires its onend handler, which unconditionally confirms the transcription. Cancelled recordings therefore still inserted the accumulated transcript.
Reset the transcription at the start of each web speech session and detach the onend handler on cancel so cancelled recordings no longer confirm.
Fixes#26784
get_chat_by_id sent admins down a branch that returned the chat only when
ENABLE_ADMIN_CHAT_ACCESS was on, or the chat was internal, and never fell
through to the access-grant and shared-folder checks. With the setting off,
an admin was therefore denied a chat that had been deliberately shared with
them, either directly or through a shared folder, while any non-admin holding
the same grant could open it. The admin role removed access the user had been
given rather than only closing the admin-only path.
Try the admin path first, then let everyone fall through to the grant and
folder checks. ENABLE_ADMIN_CHAT_ACCESS=false still closes the admin-only
route to other users' chats, and internal chats stay reachable.
* fix: let only the attendee set their own calendar RSVP status
set_attendees took each attendee's status from the caller-supplied value, so an
event organiser could set another user's RSVP (for example to 'accepted') on
create or update. RSVP is meant to be self-service: the /events/{id}/rsvp
endpoint already scopes status changes to the calling user.
Derive attendee status server-side instead of from the request. An existing
attendee keeps the status they set via RSVP and a newly added attendee starts
'pending'; any caller-supplied status is ignored. Event edits no longer reset
attendees' existing responses.
Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>
* fix: hide declined calendar invites from the attendee view
`get_events_by_range` surfaced every event where the user is an attendee regardless of their RSVP status, so declining an invite left it in the calendar with no way to remove it. Exclude `declined` attendee rows from the attendee branch, so a decline now removes the event from the user's own view while pending, accepted and tentative invitations still surface.
Co-Authored-By: legobattman <302282032+legobattman@users.noreply.github.com>
---------
Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>
The Function and Tool database columns declare user_id as a nullable
String column, but their Pydantic read-models required a non-null
string. A record with user_id NULL therefore raised a
pydantic ValidationError inside get_functions()/get_tools(), which run
during install_tool_and_function_dependencies() at app startup —
crashing the whole application and blocking all chat completions.
Make user_id Optional in the read/response models so such records
validate gracefully (user is already rendered as None downstream when
the id has no matching user) instead of taking down startup.
Claude-Session: https://claude.ai/code/session_01Y4RRUNq7ZUFkRWbWPkDw3m
Co-authored-by: Claude <noreply@anthropic.com>
On narrow viewports the admin Models list rows previously collapsed into
unusable vertical stacks; the Models page redesign on dev has since
absorbed the truncation fixes this branch carried (min-w-0 chain, real
truncate on the name, shrink-0 action group, inline access label).
The one remaining gap: the per-row edit pencil duplicates the row tap
(both open the model editor) while costing scarce horizontal space on
mobile. Hide it below the sm breakpoint; it remains on sm+ screens.
AccessGrantsPermissions only declared allow_users, so allow_groups was
missing from the model backing the default user permissions endpoints.
Pydantic ignores undeclared fields, so POST /users/default/permissions
dropped allow_groups before model_dump(), it never reached the persisted
user.permissions config, and fill_missing_permissions restored it to the
default on the next read. Turning "Allow Sharing With Groups" off in
Admin Settings silently reverted to on, while the same toggle worked when
set per group, since group permissions are stored as a plain dict.
GET /users/default/permissions and /users/default/permissions/defaults
dropped it from their responses for the same reason.
Declare allow_groups on the model so it round-trips, matching the
access_grants block in DEFAULT_USER_PERMISSIONS. It defaults to True, so
existing payloads that omit it are unaffected.
This PR fixes two small errors in the German translation file:
- "kaann" -> "kann"
- "AAlle" -> "Alle"
No functional changes, only UI strings.
Co-authored-by: Tim Baek <tim@openwebui.com>
The channel message update and delete handlers enforced authorship only on group and dm channels. On standard channels the else branch accepted any caller holding write access on the channel, so a member who could post could also edit or delete messages authored by other members. Because the update form binds content, data and meta, and the model layer never touches message.user_id, an edited message kept the original author's attribution, so another member's message could be rewritten under their name.
Write access on a channel is the capability to post, not a moderation capability, and the frontend gates the edit and delete controls on authorship (message.user_id === user.id, or admin) for every channel type. The group and dm branch already encodes this with an explicit authorship check. Apply the same rule to the standard branch: the caller must hold write access on the channel and be the message author, unless they are an admin. Pinning is unchanged, since it is exposed to every member by design.
Since v0.10.0 chat messages are virtualized with content-visibility: auto to skip rendering of off-screen messages. Safari's implementation of content-visibility has known paint bugs (WebKit bugs 277573, 281570 and 283846) that can leave the contents of a message unpainted even when it is on screen. On iOS this makes assistant responses render as empty, both in Safari and as a PWA, while the same chats render fine in Chromium and Firefox. This matches the regression window reported in #26712, which appeared when upgrading from 0.9.6 to 0.10.2.
Detect Safari (including all iOS browsers, which use WebKit) with the same user agent check already used in MessageInput and ShareChatModal, and skip the virtualization class there. Safari falls back to rendering all messages like before v0.10.0, while other engines keep the optimization. Verified with a spoofed Safari user agent that messages render without the virtualization class and with content-visibility resolving to visible, while Chromium keeps content-visibility: auto.
Fixes#26712
The sandboxed Pyodide host (used when ENABLE_PYODIDE_FILE_PERSISTENCE is
disabled, the default) embeds its script in a String.raw template. The
matplotlib show() override was written with '\\t' escapes as if in a normal
string context, but String.raw preserves them verbatim, so the iframe's
script parser turns them into literal backslash-t characters in the
generated Python source. Pyodide then fails to compile any code that
triggers the matplotlib patch with "SyntaxError: unexpected character
after line continuation character", which is why matplotlib only worked
with the file persistence worker path enabled.
Use single '\t' escapes instead: String.raw keeps them as-is in the
script text and the sandbox's JS parser produces real tab indentation,
matching the working implementation in pyodide.worker.ts.
Fixes#26660
Assistant responses are now stored as structured output items on message.output, with message.content left empty. The action payload built in chatActionHandler still sent message.content only, so action functions received assistant messages with an empty content property. Derive the content from the structured output via getOutputText, falling back to message.content, matching how the rest of Chat.svelte resolves assistant text.
Fixes#26672
_ssrf_safe_new_conn unpacks each entry of self.socket_options straight into socket.setsockopt(), which accepts exactly 3 positional arguments. urllib3-future, a drop-in fork that shadows the urllib3 package whenever it is installed (for example as a dependency of niquests pulled in through a tool or function's requirements), declares its default socket options with a per-protocol 4th element: [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1, "tcp")]. Its own _set_socket_options() strips that element before calling setsockopt(), but our override does not, so with urllib3-future present every synchronous web fetch (fetch_url, web search loading) fails on connect with "TypeError: setsockopt() takes exactly 3 arguments (4 given)" and returns empty content.
Mirror urllib3-future's handling in the override: for 4-element options whose last element is a protocol string, apply "tcp" options truncated to the first 3 elements and skip "udp" options (all sockets created here are SOCK_STREAM). Plain 3-element options, and any other shapes stock urllib3 would accept, are passed through unchanged, so behavior with stock urllib3 (which only ever uses 3-element tuples) is identical.
Verified locally: with urllib3-future installed the loader previously raised the TypeError on every URL and now fetches successfully; with stock urllib3 2.3.0 and 2.7.0 fetches behave the same before and after.
Note: #26015 reported this same crash but attributed it to stock urllib3 2.x, which only uses 3-element tuples; the 4-element form comes from urllib3-future shadowing urllib3.
Fixes#26791
is_regex_pattern only recognized the BRE-escaped form \| and not a bare |,
so a pattern like "Jornak|Silverlake|Orissa" was treated as one literal
string (including the pipe characters) and silently returned no matches.
This contradicted the tool docstring, which explicitly advertises
"error|warn" as an auto-detected regex example, and misled models into
concluding the searched terms were absent from the file.
Checking for a bare | also covers the escaped form, since \| contains |,
and normalize_regex already converts escaped pipes before compilation.
Literal patterns without regex metacharacters are unaffected.
Fixes#26781