Commit graph

17710 commits

Author SHA1 Message Date
Timothy Jaeryang Baek
f91ac068d0 refac 2026-07-27 02:12:40 -04:00
Timothy Jaeryang Baek
6c59ef313f refac 2026-07-27 02:11:10 -04:00
Classic298
6d4c02a89e
refac: owner-bind ephemeral web-search RAG collections (#26706)
The web-search-* namespace was the one collection namespace filter_accessible_collections admitted unconditionally for any non-admin user, on both read and write, unlike file-*, user-memory-* and knowledge bases which are owner-scoped. process_web_search now mints these ephemeral per-query collections as web-search-{user.id}-<hash>, and the access helper only admits web-search-{requester.id}-* names, so a web-search collection is readable and writable only by the user who created it (admins keep their bypass). The collections hold transient public web-search results and their names are non-enumerable query hashes, so there was no demonstrated cross-user access path; this removes the namespace exception so the per-user scoping the other namespaces enforce also covers web-search.

Co-authored-by: rexpository <rexpository@users.noreply.github.com>
2026-07-27 02:08:04 -04:00
Classic298
3a9b9a1a74
fix: resolve terminal system_oauth token server-side instead of trusting a client header (#26719)
The terminal proxy's system_oauth auth type read the OAuth access token from the client-supplied x-oauth-access-token request header and forwarded it verbatim as a Bearer token to the upstream terminal server, so an authenticated caller could substitute an arbitrary token for the one bound to their own session. Resolve the token server-side from the caller's OAuth session via oauth_manager.get_oauth_token(user.id, oauth_session_id), matching the openai.py proxy, so the forwarded token is always the one Open WebUI issued for the authenticated user and the client header is ignored.

Co-authored-by: brodmart <brodmart@users.noreply.github.com>
2026-07-27 02:07:34 -04:00
Classic298
6c7478c1c9
fix: surface web search embedding failures in the chat UI instead of silently returning an empty collection (#26883)
Previously, when web search retrieved pages successfully but saving them to the vector DB failed (for example an unreachable or misconfigured embedding endpoint), process_web_search swallowed the exception at debug log level and still returned status: True with the collection name. The chat then showed "Searched N sites" followed by "No sources found" at retrieval time, hiding the actual misconfiguration from the user and making the failure look like a search bug.

process_web_search now logs the failure at exception level and raises an HTTPException with an actionable message pointing at the embedding configuration in Admin Settings > Documents. chat_web_search_handler surfaces the detail of any HTTPException raised during the search in the emitted error status, so the real cause (embedding misconfiguration, search engine errors, no results) is shown in the chat UI instead of the generic "An error occurred while searching the web". Non-HTTP exceptions keep the generic message, so raw internal error strings are not exposed.

Ref #26750, #25038
2026-07-27 02:07:14 -04:00
Timothy Jaeryang Baek
3492021361 refac 2026-07-27 02:04:44 -04:00
Classic298
e17db990af
fix: parse .msg uploads via unstructured instead of extract_msg (#26704)
The .msg branch routed to langchain's OutlookMessageLoader, which requires the extract_msg package. extract_msg pins beautifulsoup4<4.14, but we pin unstructured==0.22.31 (needs beautifulsoup4>=4.14.3) and beautifulsoup4==4.14.3, so extract_msg can never be installed alongside the current dependency set. As a result the .msg path could not function on any supported install: uploads failed at runtime with an ImportError, and adding the missing package broke the build with an unsatisfiable resolver error.

Switch to UnstructuredEmailLoader, which parses .msg through unstructured's partition_msg (backed by python-oxmsg). Both are already shipped, so .msg uploads work with no new dependency and no version conflict. Attachment partitioning is disabled to preserve the previous body-only extraction behaviour.

Fixes #26690
2026-07-27 02:01:04 -04:00
Timothy Jaeryang Baek
304cbe4569 refac 2026-07-27 02:00:04 -04:00
Timothy Jaeryang Baek
c4f5ac65ee refac 2026-07-27 01:59:17 -04:00
Sebastian
8f9e9398f8
fix(docker): make open_webui/static writable by an arbitrary UID (OpenShift) (#26664)
The backend rewrites its bundled static assets under open_webui/static on
startup. Under OpenShift's restricted SCC the container runs as a random UID
(member of GID 0), which cannot write to the root-owned static dir, so boot
logs fill with '[Errno 13] Permission denied: .../static/*'.

Give GID 0 the owner's permissions on that directory (chgrp 0 + chmod g=u),
the standard Red Hat arbitrary-UID idiom. Applied unconditionally since the
app writes there on every start; complements the opt-in USE_PERMISSION_HARDENING.
2026-07-27 01:55:53 -04:00
Classic298
897d69a35c
fix: enforce feature permissions on the legacy chat-features block (image_generation, web_search) (#26703)
The legacy features block in process_chat_payload honoured client-supplied features.image_generation and features.web_search flags and dispatched to the image generation/edit provider and the web-search provider without re-checking the per-user permission that the direct /images routes and the native function-calling path enforce. A user denied features.image_generation or features.web_search could still trigger billable server-side image generation or web search via POST /api/chat/completions with params.function_calling set to legacy. Gate both branches on admin-or-has_permission before invoking chat_image_generation_handler / chat_web_search_handler, matching the existing code_interpreter gate, so a forged flag from an unpermitted user is ignored. Normal completions and permitted users are unaffected.
2026-07-27 01:54:00 -04:00
Classic298
f65f893ff1
perf: stop refetching the model row and user groups in the completion access check (#27378)
The chat completion entry point fetched the model row and then check_model_access immediately fetched the exact same row again. Inside the check, the direct grant lookup and every hop of the base-model chain each refetched the caller's group memberships, because neither call passed user_group_ids even though both AccessGrants.has_access and has_base_model_access already accept it.

check_model_access now takes an optional prefetched model_info (used only when its id matches the requested model, so stale callers cannot bypass the lookup) and resolves the caller's group ids once, sharing them across the direct check and the whole base-model chain. The group fetch is skipped entirely for the owner-with-no-base-chain case, which previously needed no groups either.

DB round trips for one completion-entry access check (non-owner model with one base-model hop):

| queries | before | after |
| --- | --- | --- |
| model row SELECTs | 3 | 2 |
| group membership SELECTs | 2 | 1 |

For deeper base-model chains the before column grows by one group SELECT per hop; the after column stays at one.

Functionally verified with stubbed model, group and grant accessors: owner fast path issues no group or grant queries; a non-owner with a base chain resolves groups once and passes the same set to every hop; a prefetched matching model_info skips the duplicate row fetch while a mismatched one is refetched; denial and unknown-model cases still raise; the arena path is unchanged.
2026-07-27 01:52:04 -04:00
Classic298
3fe829acc2
fix: strip model params for read-only callers in the model list endpoint (#27004)
The per-id model endpoint (GET /api/v1/models/model) strips params, the system
prompt and other curated model config, for callers who only have read access.
The list endpoint (GET /api/v1/models/list) did not: it returned each
read-accessible model's full params, so a read-shared model exposed its
params.system to non-owner read-grant holders.

Mirror the per-id behaviour: compute write_access per item and drop params
before serialising when the caller lacks write access (not the owner, not an
admin under BYPASS_ADMIN_ACCESS_CONTROL and holding no write grant). The
model-card list UI does not render params, so this does not change
functionality.

Co-authored-by: bogdancherniy11-sudo <229690748+bogdancherniy11-sudo@users.noreply.github.com>
2026-07-27 01:51:29 -04:00
Classic298
c05de13b4f
fix: do not expose tool source code to read-only users (#27005)
* fix: do not expose tool source code to read-only users

The tool read endpoints build their responses from a content-bearing model via
model_dump() under ConfigDict(extra='allow'). ToolResponse deliberately omits
content (the Python source) and specs, but extra='allow' re-admits both, and the
get_tools defer_content flag was a no-op, so GET /tools/, GET /tools/list and GET
/tools/id/{id} returned a tool's full source to any caller with mere read access,
including any authenticated user for a publicly read-shared tool. Tool source
commonly embeds hard-coded credentials and internal URLs.

Strip content and specs for callers without write access across the three read
endpoints. Tool execution loads source server-side, so tool use is unaffected,
and writers still receive content where they did before. The duplicated
write-access check is extracted into a small helper.

Co-authored-by: bogdancherniy11-sudo <229690748+bogdancherniy11-sudo@users.noreply.github.com>

* fix: limit the tool source strip to the per-id endpoint

Upstream dev has since fixed the defer_content no-op in Tools.get_tools, so the list endpoints (GET /tools/ and GET /tools/list) no longer fetch tool source at all and the stripping added there is redundant. Stripping specs also broke the chat Available Tools modal, which lists a tool's functions from specs for every user who can use the tool.

Reduce the change to the one remaining leak: GET /tools/id/{id} builds its response from a full model_dump() and ConfigDict(extra='allow') re-admits content, so drop content there for callers without write access. Specs stay visible to read users as before and the helper functions are no longer needed.

---------

Co-authored-by: bogdancherniy11-sudo <229690748+bogdancherniy11-sudo@users.noreply.github.com>
2026-07-27 01:51:01 -04:00
catty42
9562f1a67d
fix: honor grep -c and -l flags for piped input in kb_exec (#26721)
Co-authored-by: yuki4266 <258261435+yuki4266@users.noreply.github.com>
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-07-27 01:49:02 -04:00
Classic298
d29685275b
perf: drop the full-payload deepcopy in the OpenAI to Ollama conversion (#27371)
convert_payload_openai_to_ollama deep-copied the entire request payload on every completion routed to an Ollama model, and again on every tool-call iteration. The cost of that copy scales with the number of messages and nested content parts in the history, so long chats pay the most, purely as CPU work before the request even leaves the server.

The function only ever mutates two things: it deletes keys on the top-level dict and on the nested options dict. convert_messages_openai_to_ollama already builds fresh message dicts. Shallow-copying exactly those two levels therefore preserves behavior while removing the whole-tree copy.

Benchmark (per conversion call):

| payload | before | after | speedup |
| --- | --- | --- | --- |
| 200-message text chat (~180 KB) | 0.22 ms | 0.057 ms | 4x |
| 20-message chat + 1 MB base64 image | 0.41 ms | 0.38 ms | 1.1x |

The image row barely moves because deepcopy shares immutable strings; the win comes from container-heavy histories, which are exactly the payloads that grow over a conversation's lifetime.

The output is byte-identical to the previous implementation (verified against it, including dict key order, root parameter hoisting, max_tokens remapping, stop handling and response_format precedence), and the caller's payload is left unmodified exactly as before.
2026-07-27 01:47:23 -04:00
Classic298
915ef7d079
fix: restrict folder deletion to the owner or an admin (#27003)
* fix: restrict folder deletion to the owner or an admin

Deleting a folder cascades into the folder owner's chats, messages and the
entire subfolder subtree; the cascade is bound to the folder's owner, not the
caller. The delete handler only enforced owner/admin for root folders.
Subfolder deletion required merely write access, and a write grant on a shared
root folder is inherited by every descendant subfolder. A write-collaborator
could therefore permanently delete the owner's chats by deleting a subfolder of
a shared folder, data they do not own. With delete_contents=false the same path
force-moved the owner's chats out of the folder instead.

This also contradicted the documented sharing model: only the owner or an admin
may delete a shared folder, and write access covers adding and editing chats and
subfolders, not removing the folder.

Because any folder deletion cascades into the owner's data, restrict it to the
owner or an admin for root and subfolders alike, replacing the root/subfolder
split with a single check. Owners and admins are unaffected, and a
write-collaborator can still create, rename and add to shared folders and delete
subfolders they own.

Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>

* style: condense the folder deletion authorization comment

Shorten the multi-line comment above the owner-or-admin check to a single line stating why deletion is restricted. The full rationale lives in the pull request description and does not need to be narrated in the code.

---------

Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>
2026-07-27 01:46:58 -04:00
Timothy Jaeryang Baek
305880f2e2 refac 2026-07-27 01:46:10 -04:00
Timothy Jaeryang Baek
7801909d27 a11y
Co-Authored-By: Vince Castillo, PhD <154394560+professorcastillo@users.noreply.github.com>
2026-07-27 01:43:58 -04:00
Classic298
bc600d3f08
fix: escape KaTeX render-error fallback to prevent XSS via {@html} (#26718)
KatexRenderer rendered the raw math source through {@html} whenever renderToString threw. throwOnError only suppresses KaTeX ParseError, so a RangeError (maximum call stack size exceeded, reachable with deeply-nested brace input) escaped into the catch and re-exposed the unescaped source. Because the math tokenizer captures everything between the delimiters verbatim, that source can carry an HTML/JS payload which then executed in the viewer's browser on the application origin, a stored, cross-user XSS reachable through normal chat/channel/shared-chat rendering. Escape the fallback so the source is shown as text and is never injected as HTML. Valid math is unaffected, it still renders through the success path.

Co-authored-by: maxntv <maxntv@users.noreply.github.com>
2026-07-27 01:36:41 -04:00
Timothy Jaeryang Baek
067cf31f40 refac 2026-07-27 01:32:41 -04:00
Timothy Jaeryang Baek
8a90bf6256 chore: format 2026-07-27 01:22:08 -04:00
G30
cce3b68265
fix: enforce a single open user profile preview across ProfilePreview instances (#27578) 2026-07-27 01:21:57 -04:00
Timothy Jaeryang Baek
def26ce266 refac 2026-07-27 01:21:32 -04:00
Timothy Jaeryang Baek
75e54bf46b refac 2026-07-27 01:21:00 -04:00
Timothy Jaeryang Baek
89caa7c849 refac 2026-07-27 01:19:34 -04:00
Vince Castillo, PhD
6379d37863
fix: expose ConfirmDialog with dialog role and label its input (WCAG 4.1.2, 3.3.2) (#26769)
ConfirmDialog trapped focus and closed on Escape but its container was a plain
  div, so screen readers did not announce it as a modal dialog. Its text input
  also had only a placeholder, giving no persistent accessible name. Add
  role=dialog / aria-modal / aria-label / tabindex to the dialog surface and an
  aria-label to the textarea.

  Relates to #2790

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-07-27 01:15:12 -04:00
Timothy Jaeryang Baek
e8f2c123e6 refac 2026-07-27 01:13:09 -04:00
Classic298
7e96c53a20
feat: multiselect valve input type with static or dynamic options (#26884)
Adds a multiselect input type for Valves and UserValves so plugin authors can let users pick multiple values from static or runtime-resolved options instead of maintaining comma-separated text fields with hardcoded allowed-value lists in the description.

ENABLED_ITEMS: list[str] = Field(
    default=["foo"],
    json_schema_extra={"input": {"type": "multiselect", "options": "get_item_options"}},
)

@classmethod
def get_item_options(cls):
    return [{"value": "foo", "label": "Foo"}, {"value": "bar", "label": "Bar"}]

Options accept the same shapes as the existing select input: either a static list (strings or {value, label} dicts) or a classmethod name resolved at request time (including __user__ context for UserValves). No backend changes are needed because resolve_valves_schema_options already resolves options independently of the input type.

The new MultiSelect component follows the existing Select portal dropdown pattern and renders checkbox rows that stay open while toggling, with the selected labels shown in the trigger. Values bind as a real string array end to end: the array-to-comma-string conversions in the chat controls valves panel and the valves modal are skipped for multiselect fields, so the stored valve is a native list[str] validated by Pydantic.

Requested in #26848.
2026-07-27 01:11:38 -04:00
Timothy Jaeryang Baek
051a1f6c41 refac 2026-07-27 01:10:58 -04:00
Timothy Jaeryang Baek
6732852ce6 refac 2026-07-27 01:05:52 -04:00
Timothy Jaeryang Baek
de681aa543 refac 2026-07-27 01:03:10 -04:00
Classic298
b40b6fd698
fix: reject backslash in the terminal proxy path sanitizer (#27198)
_sanitize_proxy_path decodes the path and then relies on posixpath.normpath plus a leading '..' check. posixpath splits on '/' only, so a backslash run is treated as part of a single path component: 'foo/..\..\etc' normalizes to itself, does not start with '..' and is forwarded unchanged, reaching the upstream as '/foo/..%5C..%5Cetc'. An upstream that treats the backslash as a separator would resolve those '..' sequences.

Reject any path containing a backslash after decoding, matching the existing fail-closed behaviour for paths that are still encoded past the decode cap. A backslash is not meaningful in the upstream API paths this route proxies, so legitimate requests are unaffected.

Co-authored-by: babakizo420 <babakizo420@users.noreply.github.com>
2026-07-27 01:01:10 -04:00
Classic298
e30ed01b05
perf: stream pure passthrough proxy responses by network chunk instead of by line (#27384)
stream_wrapper without a content handler iterates aiohttp's response.content, which reads line by line: every line costs a buffer scan, a slice, a bytes concat, a generator resume and its own ASGI response message. A typical SSE event is two lines (the data line and the blank separator), so every upstream token event became two yields and two transport writes even on routes where the body is never inspected.

stream_wrapper now takes passthrough=True, which iterates response.content.iter_any(): the exact same bytes, one yield per network read, no line scanning. It is applied only to routes no internal consumer parses line-by-line: the ollama pull/push/create/generate proxies and its v1 completions, chat completions, messages and responses endpoints, plus the openai embeddings, responses and catch-all proxies. The two internally consumed chat routes keep line iteration, which the streaming middleware and the Ollama-to-OpenAI converter require; the ollama send_request signature documents that constraint.

Benchmark (local aiohttp SSE server, 500 events, consumed through stream_wrapper):

| metric | before (readline) | after (iter_any) |
| --- | --- | --- |
| stream consumption time | 1.46 ms | 0.62 ms |
| generator yields + response writes per stream | 1000 | 1 |

The single yield is a loopback artifact (the whole body arrives in one buffered read); over a real network it becomes one yield per TCP read instead of two per SSE event.

Functionally verified: line mode and passthrough mode produce byte-identical output for the same stream, and passthrough always yields fewer, larger chunks.
2026-07-27 00:58:24 -04:00
Classic298
5dcca59aee
fix: route OAuth profile-picture fetch through the SSRF-safe session (#26699)
_process_picture_url validated the picture URL with validate_url() but then fetched it with a plain aiohttp session that resolves the hostname again at connect time, leaving a DNS-rebinding TOCTOU window (the same gap already closed for the RAG loader, the content probe, the image fetches and webhook delivery). Routing the fetch through get_ssrf_safe_session() pins the connect-time resolution via _SSRFSafeResolver and rejects non-global addresses, so a rebinding host can no longer redirect the fetch to loopback, RFC1918 or cloud-metadata endpoints. It also stops the forwarded OAuth access_token from leaking to a rebound internal target.
2026-07-27 00:56:24 -04:00
Timothy Jaeryang Baek
dd86b984bd refac 2026-07-27 00:55:16 -04:00
Timothy Jaeryang Baek
1717b493d8 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-07-27 00:54:28 -04:00
Timothy Jaeryang Baek
5c505c1119 refac 2026-07-27 00:48:30 -04:00
G30
085d11eef2
chore: drop redundant background repaints so surfaces inherit their parent (#27576)
* chore: drop redundant background repaints so surfaces inherit their parent

Four spots repaint the exact color their parent surface already provides
(bg-white / dark:bg-gray-900 rows inside same-colored pages and modals,
and the selectClass dark repaint inside the connection modals — the
sibling input const is already fully transparent). Visually identical in
stock light and dark; removing them lets instance theming show through
instead of leaving opaque boxes:

- .tiptap tr (app.css) — table rows in notes/editors
- Edit User Group Users tab body rows (common Modal surface)
- AddToolServerModal + AddTerminalServerModal selectClass dark repaint

The matching repaints inside the ModelUsage/UserUsage components are
not part of this change — those files were dead code and were removed
entirely in #27574.

* chore: catch remaining redundant surface repaints missed in the first pass

Same rule as the previous commit — every one of these repaints the exact
color its parent surface already provides, so removal is stock-identical
in light and dark while letting instance theming show through:

- Analytics Dashboard's inline Model Usage / User Activity row markup
  (the Analytics tab renders these tables from Dashboard.svelte itself;
  the unreferenced ModelUsage/UserUsage component files were removed
  in #27574)
- Evaluations Feedbacks + Leaderboard body rows (settings modal surface)
- admin UserList body rows (app page surface)
- chat markdown tables (MarkdownTokens): thead and body rows — unlike
  the tiptap header (gray-850 contrast, untouched), this thead painted
  the page's own color
- CitationsModal source rows (common Modal surface)
- AddConnectionModal selectClass dark repaint — third copy of the same
  const already fixed in AddToolServerModal / AddTerminalServerModal
2026-07-27 00:44:51 -04:00
Classic298
41573d52f1
fix: require an authenticated user on the Ollama version route (#27199)
get_ollama_versions was the only Ollama route besides the static health check without an authentication dependency, so an anonymous caller could read the configured backend's version string and, by walking url_idx until the lookup raised, count the configured backends.

Nothing depends on the route being public. The frontend wrapper takes a token and sends it on every call, and its three call sites (admin model management, the model selector and the About panel) all pass an authenticated token, so the client already treats this as an authenticated route. Add the same get_verified_user dependency the sibling routes carry.

Co-authored-by: Grg0rry <Grg0rry@users.noreply.github.com>
2026-07-27 00:44:31 -04:00
G30
8295f2dacc
chore: remove dead admin Analytics ModelUsage and UserUsage components (#27574)
Nothing in the tree imports either component; the admin Analytics tab
renders its own inline copies of both tables directly from
Dashboard.svelte. Both files landed with the dashboard in a4ad34841
(feat: analytics frontend dashboard) but were never wired into it.

The remaining name matches elsewhere (the getUserUsage API and
UserUsage* types in src/lib/apis/users/index.ts, consumed by
chat/Settings/Usage.svelte, plus the backend usage endpoints) belong to
the unrelated per-user usage feature and are untouched.
2026-07-27 00:35:08 -04:00
Timothy Jaeryang Baek
55e0801dab refac 2026-07-27 00:34:25 -04:00
EntropyYue
f21d7947f9
fix: Set default Redis socket timeout to None (#27104) 2026-07-27 00:30:00 -04:00
Timothy Jaeryang Baek
57e60423b9 refac 2026-07-27 00:27:38 -04:00
Timothy Jaeryang Baek
20647bd2d5 chore: format 2026-07-27 00:12:47 -04:00
Timothy Jaeryang Baek
e53ff57fb5 refac 2026-07-27 00:12:16 -04:00
Timothy Jaeryang Baek
c727643e05 refac 2026-07-27 00:11:59 -04:00
Timothy Jaeryang Baek
4a7d4ebada refac 2026-07-27 00:10:36 -04:00
Timothy Jaeryang Baek
8ddf119570 refac 2026-07-26 23:55:37 -04:00
Timothy Jaeryang Baek
e5a08d5220 refac 2026-07-26 23:54:16 -04:00