From a0268e51fc12c33b6df447bead4909ecd6733ebc Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 9 May 2026 13:56:06 +0200 Subject: [PATCH 01/37] Merge pull request #24486 from Classic298/fix/notes-is-pinned-typeerror fix: notes is_pinned TypeError on create/get --- backend/open_webui/models/notes.py | 2 +- backend/open_webui/routers/notes.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index a665004a95..f651d226ca 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -140,7 +140,7 @@ class NoteTable: } ) - new_note = Note(**note.model_dump(exclude={'access_grants'})) + new_note = Note(**note.model_dump(exclude={'access_grants', 'is_pinned'})) db.add(new_note) await db.commit() diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 9a23a104c9..5ed46b5d61 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -294,7 +294,10 @@ async def get_note_by_id( ) pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) - return NoteResponse(**note.model_dump(), write_access=write_access, is_pinned=note.id in pinned_note_ids) + return NoteResponse( + **{**note.model_dump(), 'is_pinned': note.id in pinned_note_ids}, + write_access=write_access, + ) ############################ From 793e628ac30c711f47ebe008ec2cf120488c4fa9 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 9 May 2026 20:59:29 +0900 Subject: [PATCH 02/37] refac --- .github/pull_request_template.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ad311a371a..daf908c02d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,18 +6,9 @@ # Pull Request Checklist -### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) to discuss your idea/fix with the community before creating a pull request, and describe your changes before submitting a pull request. - -This is to ensure large feature PRs are discussed with the community first, before starting work on it. If the community does not want this feature or it is not relevant for Open WebUI as a project, it can be identified in the discussion before working on the feature and submitting the PR. - - - **Before submitting, make sure you've checked the following:** +- [ ] **Linked Issue/Discussion:** This PR references an existing [Issue](https://github.com/open-webui/open-webui/issues) or [Discussion](https://github.com/open-webui/open-webui/discussions) — `Closes #___` / `Relates to #___`. If one does not exist, create one first. PRs without a linked issue or discussion may be closed without review. - [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.** - [ ] **Description:** Provide a concise description of the changes made in this pull request down below. - [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description. From 88545415088e3eba8572711d444c6b914a9eb095 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 9 May 2026 14:01:45 +0200 Subject: [PATCH 03/37] fix: prevent redirect-based SSRF in web-fetch and image-load call sites (#24491) 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 --- backend/open_webui/retrieval/utils.py | 6 +++++- backend/open_webui/retrieval/web/utils.py | 11 +++++++++++ backend/open_webui/routers/images.py | 8 ++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 14a64fed60..4c676705f0 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -180,8 +180,12 @@ def get_content_from_url(request, url: str) -> str: validate_url(url) # Streamed GET to check Content-Type without downloading the body. + # allow_redirects=False prevents redirect-based SSRF: validate_url() above is + # called on the originally-submitted URL only; following 3xx redirects without + # re-validation would let an attacker reach private IPs (RFC1918, loopback, + # cloud-metadata 169.254.169.254) via a public host that redirects internally. try: - response = requests.get(url, stream=True, timeout=30) + response = requests.get(url, stream=True, timeout=30, allow_redirects=False) response.raise_for_status() content_type = response.headers.get('Content-Type', '') except Exception: diff --git a/backend/open_webui/retrieval/web/utils.py b/backend/open_webui/retrieval/web/utils.py index 6ee0e3781a..f745c296e1 100644 --- a/backend/open_webui/retrieval/web/utils.py +++ b/backend/open_webui/retrieval/web/utils.py @@ -485,6 +485,17 @@ class SafeWebBaseLoader(WebBaseLoader): """ super().__init__(*args, **kwargs) self.trust_env = trust_env + # Prevent redirect-based SSRF on the synchronous _scrape() path. + # validate_url() is called once on the originally-submitted URL, but the + # parent WebBaseLoader's _scrape() invokes self.session.get(url, **self.requests_kwargs) + # which by default follows redirects. Without the override below, an attacker + # can submit a public URL that 302-redirects to an internal address (RFC1918, + # 127.0.0.1, 169.254.169.254, etc.) and the redirected target is fetched without + # re-validation. Matches the policy enforced on the async _fetch() path below. + self.requests_kwargs = { + **(self.requests_kwargs or {}), + 'allow_redirects': False, + } async def _fetch(self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5) -> str: async with aiohttp.ClientSession(trust_env=self.trust_env) as session: diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 7ef4938b2e..f1559bc034 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -807,10 +807,14 @@ async def image_edits( return data if data.startswith('http://') or data.startswith('https://'): - # Validate URL to prevent SSRF attacks against local/private networks + # Validate URL to prevent SSRF attacks against local/private networks. + # allow_redirects=False prevents redirect-based SSRF: validate_url() is + # called only on the originally-submitted URL; following 3xx redirects + # without re-validation would let an attacker reach private IPs via a + # public host that redirects internally (e.g. cloud-metadata exfil). validate_url(data) session = await get_session() - async with session.get(data, ssl=AIOHTTP_CLIENT_SESSION_SSL) as r: + async with session.get(data, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=False) as r: r.raise_for_status() image_data = base64.b64encode(await r.read()).decode('utf-8') From 2fa3b8424198ea143178b17d354e3b171283c926 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 9 May 2026 21:04:52 +0900 Subject: [PATCH 04/37] chore: bump --- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a76993b2d..2218a39051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.5] - 2026-05-09 + +### Fixed + +- 📝 **Notes create and open reliability.** Creating new notes and opening existing notes no longer fails with a TypeError caused by `is_pinned` being passed to the SQLAlchemy model on create, and passed twice to `NoteResponse` on read. [#24484](https://github.com/open-webui/open-webui/issues/24484), [#24486](https://github.com/open-webui/open-webui/pull/24486) + ## [0.9.4] - 2026-05-09 ### Fixed diff --git a/package-lock.json b/package-lock.json index cbca943492..d39b8f2060 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.9.4", + "version": "0.9.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.9.4", + "version": "0.9.5", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", diff --git a/package.json b/package.json index 08b50a26bb..04cd864710 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.9.4", + "version": "0.9.5", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", From df42d96c95784b8abe430b935902e42c9ed15205 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 9 May 2026 21:05:49 +0900 Subject: [PATCH 05/37] refac --- backend/open_webui/env.py | 7 +++++++ backend/open_webui/retrieval/utils.py | 3 ++- backend/open_webui/retrieval/web/utils.py | 6 +++--- backend/open_webui/routers/images.py | 4 ++-- backend/open_webui/utils/code_interpreter.py | 4 +++- backend/open_webui/utils/oauth.py | 3 ++- backend/open_webui/utils/tools.py | 5 +++-- 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 8a9b3af365..1ab18fe1c7 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -824,6 +824,13 @@ else: AIOHTTP_CLIENT_SESSION_SSL = os.environ.get('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true' +# When False (default), outbound HTTP requests do not follow 3xx redirects. +# This prevents redirect-based SSRF where a public URL 302-redirects to an +# internal address (RFC 1918, loopback, cloud-metadata 169.254.169.254). +# Set to True only if your deployment requires redirect following and you +# have other SSRF protections in place (e.g. egress firewall). +AIOHTTP_CLIENT_ALLOW_REDIRECTS = os.environ.get('AIOHTTP_CLIENT_ALLOW_REDIRECTS', 'False').lower() == 'true' + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get( 'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST', os.environ.get('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'), diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 4c676705f0..8e672b7a8f 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -43,6 +43,7 @@ from open_webui.retrieval.loaders.youtube import YoutubeLoader from open_webui.env import ( AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_CLIENT_ALLOW_REDIRECTS, OFFLINE_MODE, ENABLE_FORWARD_USER_INFO_HEADERS, AIOHTTP_CLIENT_SESSION_SSL, @@ -185,7 +186,7 @@ def get_content_from_url(request, url: str) -> str: # re-validation would let an attacker reach private IPs (RFC1918, loopback, # cloud-metadata 169.254.169.254) via a public host that redirects internally. try: - response = requests.get(url, stream=True, timeout=30, allow_redirects=False) + response = requests.get(url, stream=True, timeout=30, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS) response.raise_for_status() content_type = response.headers.get('Content-Type', '') except Exception: diff --git a/backend/open_webui/retrieval/web/utils.py b/backend/open_webui/retrieval/web/utils.py index f745c296e1..633f4bba5e 100644 --- a/backend/open_webui/retrieval/web/utils.py +++ b/backend/open_webui/retrieval/web/utils.py @@ -48,7 +48,7 @@ from open_webui.config import ( WEB_FETCH_FILTER_LIST, ) from open_webui.utils.misc import is_string_allowed -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_ALLOW_REDIRECTS log = logging.getLogger(__name__) @@ -494,7 +494,7 @@ class SafeWebBaseLoader(WebBaseLoader): # re-validation. Matches the policy enforced on the async _fetch() path below. self.requests_kwargs = { **(self.requests_kwargs or {}), - 'allow_redirects': False, + 'allow_redirects': AIOHTTP_CLIENT_ALLOW_REDIRECTS, } async def _fetch(self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5) -> str: @@ -513,7 +513,7 @@ class SafeWebBaseLoader(WebBaseLoader): async with session.get( url, **(self.requests_kwargs | kwargs), - allow_redirects=False, + allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS, ) as response: if self.raise_for_status: response.raise_for_status() diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index f1559bc034..e79b12dabb 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -22,7 +22,7 @@ from open_webui.config import ( ) from open_webui.constants import ERROR_MESSAGES from open_webui.retrieval.web.utils import validate_url -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, ENABLE_FORWARD_USER_INFO_HEADERS +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_ALLOW_REDIRECTS, ENABLE_FORWARD_USER_INFO_HEADERS from open_webui.utils.session_pool import get_session from open_webui.models.chats import Chats @@ -814,7 +814,7 @@ async def image_edits( # public host that redirects internally (e.g. cloud-metadata exfil). validate_url(data) session = await get_session() - async with session.get(data, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=False) as r: + async with session.get(data, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS) as r: r.raise_for_status() image_data = base64.b64encode(await r.read()).decode('utf-8') diff --git a/backend/open_webui/utils/code_interpreter.py b/backend/open_webui/utils/code_interpreter.py index 3e30c419ae..52ddea24a7 100644 --- a/backend/open_webui/utils/code_interpreter.py +++ b/backend/open_webui/utils/code_interpreter.py @@ -8,6 +8,8 @@ import aiohttp import websockets from pydantic import BaseModel +from open_webui.env import AIOHTTP_CLIENT_ALLOW_REDIRECTS + logger = logging.getLogger(__name__) @@ -88,7 +90,7 @@ class JupyterCodeExecuter: async with self.session.post( 'login', data={'_xsrf': xsrf_token, 'password': self.password}, - allow_redirects=False, + allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS, ) as response: response.raise_for_status() self.session.cookie_jar.update_cookies(response.cookies) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 56341bdca3..320124ba4d 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -71,6 +71,7 @@ from open_webui.config import ( from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES from open_webui.env import ( AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_ALLOW_REDIRECTS, WEBUI_NAME, WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE, @@ -740,7 +741,7 @@ class OAuthClientManager: async with aiohttp.ClientSession(trust_env=True) as session: async with session.get( authorization_url, - allow_redirects=False, + allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as resp: if resp.status < 400: diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 20e8ce365c..6489443285 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -47,6 +47,7 @@ from open_webui.utils.access_control import has_access, has_connection_access from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.env import ( AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_ALLOW_REDIRECTS, AIOHTTP_CLIENT_TIMEOUT, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA, @@ -1433,7 +1434,7 @@ async def execute_tool_server( headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, - allow_redirects=False, + allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS, ) as response: if response.status >= 400: text = await response.text() @@ -1458,7 +1459,7 @@ async def execute_tool_server( headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, - allow_redirects=False, + allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS, ) as response: if response.status >= 400: text = await response.text() From 2e71b3fbb8b8434f91f0f8879f50a5b4c1b2f34f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 9 May 2026 21:07:08 +0900 Subject: [PATCH 06/37] chore: format --- backend/open_webui/routers/images.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index e79b12dabb..f61970f3de 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -814,7 +814,9 @@ async def image_edits( # public host that redirects internally (e.g. cloud-metadata exfil). validate_url(data) session = await get_session() - async with session.get(data, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS) as r: + async with session.get( + data, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS + ) as r: r.raise_for_status() image_data = base64.b64encode(await r.read()).decode('utf-8') From 69270e1c9e383059f13660e4247a7412c62b2d26 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 9 May 2026 21:08:07 +0900 Subject: [PATCH 07/37] doc: changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2218a39051..0537f3ef3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.9.5] - 2026-05-09 +### Added + +- 🛡️ **Redirect-based SSRF protection.** All outbound HTTP requests now block 3xx redirects by default via a new `AIOHTTP_CLIENT_ALLOW_REDIRECTS` environment variable, preventing redirect-based SSRF where a public URL silently redirects to internal addresses (RFC 1918, loopback, cloud-metadata endpoints). Affected call sites include web fetch, image loading, OAuth discovery, tool server execution, and code interpreter login. [#24491](https://github.com/open-webui/open-webui/pull/24491) + ### Fixed - 📝 **Notes create and open reliability.** Creating new notes and opening existing notes no longer fails with a TypeError caused by `is_pinned` being passed to the SQLAlchemy model on create, and passed twice to `NoteResponse` on read. [#24484](https://github.com/open-webui/open-webui/issues/24484), [#24486](https://github.com/open-webui/open-webui/pull/24486) From 8a0018cf96d98f6dc99523a1423aa2ef2c638392 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 9 May 2026 16:18:51 +0200 Subject: [PATCH 08/37] fix: gate public sharing of calendars behind sharing.public_calendars permission (#24493) * fix: gate public sharing of calendars behind sharing.public_calendars permission The calendar router did not call filter_allowed_access_grants on either the create or update endpoint, while every other shareable resource in the codebase (channels, knowledge, models, notes, prompts, skills, tools) does. A verified non-admin owner could therefore attach `{"principal_type":"user","principal_id":"*","permission":"read"|"write"}` to their own calendar in the create or update payload and have it persisted unfiltered. Any other verified user with the (default-on) features.calendar permission could then read or, for write grants, write events on it via the existing /events* endpoints, bypassing the per-user sharing.public_ permission gate the rest of the resource cohort enforces. Three changes: - config.py: add USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING (default False, env-overridable) and surface it in DEFAULT_USER_PERMISSIONS ['sharing']['public_calendars'] so admins can grant it per group via the same UI used for notes/models/etc. - routers/calendar.py: import filter_allowed_access_grants and call it in create_calendar with the new sharing.public_calendars key, identical to the channel router's pattern. - routers/calendar.py: call filter_allowed_access_grants in update_calendar too. The pre-existing owner-only gate at L350 only restricts WHO may change grants; the new filter restricts WHICH grants they may set, so a non-admin owner cannot make their own calendar publicly readable or writable without the corresponding sharing permission. Same shape as GHSA-7rjh-px4v-5w55 (channels). Reported by Matteo Panzeri. Co-authored-by: Matteo Panzeri <28739806+matte1782@users.noreply.github.com> * fix: expose public_calendars + features.calendar through admin permissions surface The earlier commit added DEFAULT_USER_PERMISSIONS['sharing']['public_calendars'] and the runtime filter call, but the new key was not yet plumbed through the admin /users/default/permissions endpoint. Without these changes the toggle would round-trip as silently dropped: - routers/users.py SharingPermissions: any payload POSTed to /default/permissions ran through `form_data.model_dump()`, and Pydantic drops fields not declared on the model. The new public_calendars key would have been stripped on every save, leaving admins unable to grant the permission via the UI even though the runtime filter would honor it. - src/lib/constants/permissions.ts: the frontend's DEFAULT_PERMISSIONS dict is the seed shape used by the admin Groups Permissions panel; without the new key it could not bind a Switch component to it. - Permissions.svelte: add a Calendars Public Sharing toggle alongside the Notes/Chats Public Sharing toggles, gated on the existing features.calendar flag (matches the pattern used for notes/chats). Also closes a pre-existing parity gap on features.calendar: DEFAULT_USER_ PERMISSIONS['features']['calendar'] has existed since the calendar feature shipped, and Permissions.svelte already renders a Calendar feature toggle, but FeaturesPermissions Pydantic and the frontend defaults never knew about it. Adding it everywhere completes the round-trip so admin saves no longer silently drop the calendar feature flag either. --------- Co-authored-by: Matteo Panzeri <28739806+matte1782@users.noreply.github.com> --- backend/open_webui/config.py | 5 ++++ backend/open_webui/routers/calendar.py | 27 ++++++++++++++++++- backend/open_webui/routers/users.py | 2 ++ .../admin/Users/Groups/Permissions.svelte | 18 +++++++++++++ src/lib/constants/permissions.ts | 6 +++-- 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index dbf9fad19b..4cc39e11c4 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1465,6 +1465,10 @@ USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = ( os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' ) +USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING = ( + os.environ.get('USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = ( os.environ.get('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true' ) @@ -1585,6 +1589,7 @@ DEFAULT_USER_PERMISSIONS = { 'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING, 'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING, 'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING, + 'public_calendars': USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING, }, 'access_grants': { 'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS, diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py index c95888ebfa..bdc06e819b 100644 --- a/backend/open_webui/routers/calendar.py +++ b/backend/open_webui/routers/calendar.py @@ -22,7 +22,7 @@ from open_webui.models.access_grants import AccessGrants from open_webui.models.groups import Groups from open_webui.models.users import UserModel from open_webui.utils.auth import get_verified_user -from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants from open_webui.utils.calendar import expand_recurring_event from open_webui.constants import ERROR_MESSAGES @@ -112,6 +112,17 @@ async def get_calendars(request: Request, user: UserModel = Depends(get_verified async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)): """Create a new user calendar.""" await check_calendar_permission(request, user) + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/models pattern). Without this, any verified user + # could create a calendar with `principal_id='*' permission='read'|'write'`, + # making their events readable or writable by any other verified user. + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_calendars', + ) return await Calendars.insert_new_calendar(user.id, form_data) @@ -350,6 +361,20 @@ async def update_calendar( if form_data.access_grants is not None and cal.user_id != user.id and user.role != 'admin': raise HTTPException(status_code=403, detail='Only owner can manage sharing') + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/models pattern). The owner-only check above + # only restricts WHO can set grants; this filter restricts WHICH grants + # they may set, so a non-admin owner cannot make their calendar + # publicly readable/writable without the corresponding sharing permission. + if form_data.access_grants is not None: + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_calendars', + ) + updated = await Calendars.update_calendar_by_id(calendar_id, form_data) if not updated: raise HTTPException(status_code=500, detail='Failed to update') diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index bcf11936e2..7fe5fcd2dc 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -194,6 +194,7 @@ class SharingPermissions(BaseModel): notes: bool = False public_notes: bool = True public_chats: bool = False + public_calendars: bool = False class AccessGrantsPermissions(BaseModel): @@ -235,6 +236,7 @@ class FeaturesPermissions(BaseModel): code_interpreter: bool = True memories: bool = True automations: bool = False + calendar: bool = True class SettingsPermissions(BaseModel): diff --git a/src/lib/components/admin/Users/Groups/Permissions.svelte b/src/lib/components/admin/Users/Groups/Permissions.svelte index 313834bfdc..6523419531 100644 --- a/src/lib/components/admin/Users/Groups/Permissions.svelte +++ b/src/lib/components/admin/Users/Groups/Permissions.svelte @@ -410,6 +410,24 @@ {/if} {/if} + + {#if permissions.features.calendar} +
+
+
+ {$i18n.t('Calendars Public Sharing')} +
+ +
+ {#if defaultPermissions?.sharing?.public_calendars && !permissions.sharing.public_calendars} +
+
+ {$i18n.t('This is a default user permission and will remain enabled.')} +
+
+ {/if} +
+ {/if}
diff --git a/src/lib/constants/permissions.ts b/src/lib/constants/permissions.ts index c740696ee1..b384e301b3 100644 --- a/src/lib/constants/permissions.ts +++ b/src/lib/constants/permissions.ts @@ -25,7 +25,8 @@ export const DEFAULT_PERMISSIONS = { public_skills: false, notes: false, public_notes: false, - public_chats: false + public_chats: false, + public_calendars: false }, access_grants: { allow_users: true @@ -62,7 +63,8 @@ export const DEFAULT_PERMISSIONS = { image_generation: true, code_interpreter: true, memories: true, - automations: false + automations: false, + calendar: true }, settings: { interface: true From 9918ab62657378dc277441eddc93f0bcdefcd85c Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 9 May 2026 16:19:03 +0200 Subject: [PATCH 09/37] fix: gate public sharing of skills behind sharing.public_skills on create/update (#24494) The /create (L155-193) and /id/{id}/update (L248-297) endpoints in routers/skills.py persisted form_data.access_grants directly to AccessGrants.set_access_grants without filter_allowed_access_grants, while every other shareable resource in the codebase (channels, knowledge, models, notes, prompts, tools, calendars) and the dedicated /id/{id}/access/update endpoint on this same router (L309-348) all do call the filter. A user with workspace.skills permission (default False, but admins can grant it to skill-creating users) could therefore attach {"principal_type":"user","principal_id":"*","permission":"read"|"write"} to the create or update payload and have it persisted unfiltered, bypassing the sharing.public_skills gate that the rest of the cohort enforces. Two changes: - create_new_skill: call filter_allowed_access_grants with 'sharing.public_skills' immediately before insert, after the existing permission check and ID-taken check. - update_skill_by_id: call filter_allowed_access_grants with the same key after the access check, before form_data.model_dump() flows into Skills.update_skill_by_id. The pre-existing access check at L263-277 only restricts WHO may modify the skill; the new filter restricts WHICH grants they may set. All supporting plumbing was already in place from prior PRs: filter_allowed_access_grants is already imported at L22, the USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING constant exists, DEFAULT_USER_PERMISSIONS['sharing']['public_skills'] is wired up, SharingPermissions.public_skills is in the Pydantic, and the admin UI already renders the toggle. This is a pure 2-line router fix that closes the cohort-consistency gap. Same shape as the calendar fix in #24493, reported by Matteo Panzeri while auditing the resource-cohort cohort during follow-up on #24493. Co-authored-by: Matteo Panzeri <28739806+matte1782@users.noreply.github.com> --- backend/open_webui/routers/skills.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index 490d1706d5..ede5afd814 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -176,6 +176,19 @@ async def create_new_skill( detail=ERROR_MESSAGES.ID_TAKEN, ) + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/calendar pattern). Without this, a user with + # workspace.skills permission could attach principal_id='*' read/write + # grants in the create payload, bypassing the sharing.public_skills gate + # that the dedicated /access/update endpoint already enforces. + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_skills', + ) + try: skill = await Skills.insert_new_skill(user.id, form_data, db=db) if skill: @@ -276,6 +289,19 @@ async def update_skill_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/calendar pattern). The access check above only + # restricts WHO can write to the skill; this filter restricts WHICH grants + # they may set, so a non-admin owner cannot make their own skill publicly + # readable/writable without sharing.public_skills permission. + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_skills', + ) + try: updated = { **form_data.model_dump(exclude={'id'}), From 203ec29bafe3a8f753c83c028d9b0582cdacc837 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 9 May 2026 16:19:14 +0200 Subject: [PATCH 10/37] chore: remove unauthenticated dead-code GET /api/v1/retrieval/ status endpoint (#24497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `get_status()` handler at retrieval.py:263 (`@router.get('/')`) returned the live RAG pipeline configuration (CHUNK_SIZE, CHUNK_OVERLAP, RAG_TEMPLATE, RAG_EMBEDDING_ENGINE, RAG_EMBEDDING_MODEL, RAG_RERANKING_MODEL, etc.) without any authentication dependency, while every adjacent endpoint on the same router (/embedding, /embedding/update, /config, /config/update) requires get_admin_user. Exhaustive search of the repository confirms the endpoint has no callers: - Frontend (src/): no `RETRIEVAL_API_BASE_URL}/'`-style fetch; the existing `getRAGConfig()` in src/lib/apis/retrieval/index.ts targets `/config`, not the root, and is the only consumer of admin-level retrieval state. - Backend self-references: none. - Cypress e2e (chat, documents, registration, settings): none. - Backend tests (backend/open_webui/test/): none. - Build/CI scripts (scripts/): none. - Direct symbol import of `get_status` from this router: none. The endpoint is dead code, almost certainly a relic from before the /config GET split. Removing it has zero UX impact and eliminates the unauthenticated-config-disclosure surface raised in advisory triage on GHSA-65pg-qhhw-mxwg. External monitoring scripts that may have hit the bare root will receive a 404 and can switch to the existing /config endpoint, which returns the same fields plus the rest of the RAG config under admin auth. Surface raised by 0xRyuzak1 in GHSA-65pg-qhhw-mxwg. The advisory was closed as not-a-vulnerability per SECURITY.md Rule 1 (no security boundary crossed in default config — RAG_TEMPLATE default is a citation-format instruction, not a system prompt; no integrity/availability impact); this removal is independent code-hygiene that aligns the router cohort. Reported-by: 0xRyuzak1 --- backend/open_webui/routers/retrieval.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 274eafe8ae..dfd503f035 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -260,22 +260,6 @@ class SearchForm(BaseModel): queries: List[str] -@router.get('/') -async def get_status(request: Request): - return { - 'status': True, - 'CHUNK_SIZE': request.app.state.config.CHUNK_SIZE, - 'CHUNK_OVERLAP': request.app.state.config.CHUNK_OVERLAP, - 'RAG_TEMPLATE': request.app.state.config.RAG_TEMPLATE, - 'RAG_EMBEDDING_ENGINE': request.app.state.config.RAG_EMBEDDING_ENGINE, - 'RAG_EMBEDDING_MODEL': request.app.state.config.RAG_EMBEDDING_MODEL, - 'RAG_RERANKING_MODEL': request.app.state.config.RAG_RERANKING_MODEL, - 'RAG_EMBEDDING_BATCH_SIZE': request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, - 'ENABLE_ASYNC_EMBEDDING': request.app.state.config.ENABLE_ASYNC_EMBEDDING, - 'RAG_EMBEDDING_CONCURRENT_REQUESTS': request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, - } - - @router.get('/embedding') async def get_embedding_config(request: Request, user=Depends(get_admin_user)): return { From 55535a89653bfb7fe671d5486ce3ffafdc9b2004 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 9 May 2026 23:21:08 +0900 Subject: [PATCH 11/37] doc: changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0537f3ef3d..fb1930256b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - 📝 **Notes create and open reliability.** Creating new notes and opening existing notes no longer fails with a TypeError caused by `is_pinned` being passed to the SQLAlchemy model on create, and passed twice to `NoteResponse` on read. [#24484](https://github.com/open-webui/open-webui/issues/24484), [#24486](https://github.com/open-webui/open-webui/pull/24486) +- 🔐 **Skill public sharing permission enforcement.** Creating or updating skills now filters access grants through the `sharing.public_skills` permission, preventing non-admin users from making skills publicly accessible without the required permission. [#24494](https://github.com/open-webui/open-webui/pull/24494) +- 🔐 **Calendar public sharing permission enforcement.** Creating or updating calendars now filters access grants through a new `sharing.public_calendars` permission, preventing users from making calendars publicly readable or writable without explicit admin-granted sharing permission. [#24493](https://github.com/open-webui/open-webui/pull/24493) + +### Changed + +- 🧹 **Removed unauthenticated retrieval status endpoint.** The unauthenticated `GET /api/v1/retrieval/` status endpoint has been removed as dead code — retrieval configuration is already available through authenticated admin endpoints. [#24497](https://github.com/open-webui/open-webui/pull/24497) +- 📋 **PR template issue requirement.** Pull requests now require a linked Issue or Discussion reference, ensuring better traceability for all contributions. PRs without a linked issue or discussion may be closed without review. ## [0.9.4] - 2026-05-09 From 8689f7090f41c1bdbc088531759975fc16f0dd1e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 9 May 2026 23:22:05 +0900 Subject: [PATCH 12/37] chore: format --- src/lib/i18n/locales/ar-BH/translation.json | 1 + src/lib/i18n/locales/ar/translation.json | 1 + src/lib/i18n/locales/az-AZ/translation.json | 1 + src/lib/i18n/locales/bg-BG/translation.json | 1 + src/lib/i18n/locales/bn-BD/translation.json | 1 + src/lib/i18n/locales/bo-TB/translation.json | 1 + src/lib/i18n/locales/bs-BA/translation.json | 1 + src/lib/i18n/locales/ca-ES/translation.json | 1 + src/lib/i18n/locales/ceb-PH/translation.json | 1 + src/lib/i18n/locales/cs-CZ/translation.json | 1 + src/lib/i18n/locales/da-DK/translation.json | 1 + src/lib/i18n/locales/de-DE/translation.json | 1 + src/lib/i18n/locales/dg-DG/translation.json | 1 + src/lib/i18n/locales/el-GR/translation.json | 1 + src/lib/i18n/locales/en-GB/translation.json | 1 + src/lib/i18n/locales/en-US/translation.json | 1 + src/lib/i18n/locales/es-ES/translation.json | 1 + src/lib/i18n/locales/et-EE/translation.json | 1 + src/lib/i18n/locales/eu-ES/translation.json | 1 + src/lib/i18n/locales/fa-IR/translation.json | 1 + src/lib/i18n/locales/fi-FI/translation.json | 1 + src/lib/i18n/locales/fil-PH/translation.json | 1 + src/lib/i18n/locales/fr-CA/translation.json | 1 + src/lib/i18n/locales/fr-FR/translation.json | 1 + src/lib/i18n/locales/gl-ES/translation.json | 1 + src/lib/i18n/locales/he-IL/translation.json | 1 + src/lib/i18n/locales/hi-IN/translation.json | 1 + src/lib/i18n/locales/hr-HR/translation.json | 1 + src/lib/i18n/locales/hu-HU/translation.json | 1 + src/lib/i18n/locales/id-ID/translation.json | 1 + src/lib/i18n/locales/ie-GA/translation.json | 1 + src/lib/i18n/locales/it-IT/translation.json | 1 + src/lib/i18n/locales/ja-JP/translation.json | 1 + src/lib/i18n/locales/ka-GE/translation.json | 1 + src/lib/i18n/locales/kab-DZ/translation.json | 1 + src/lib/i18n/locales/ko-KR/translation.json | 1 + src/lib/i18n/locales/lt-LT/translation.json | 1 + src/lib/i18n/locales/lv-LV/translation.json | 1 + src/lib/i18n/locales/ms-MY/translation.json | 1 + src/lib/i18n/locales/nb-NO/translation.json | 1 + src/lib/i18n/locales/nl-NL/translation.json | 1 + src/lib/i18n/locales/pa-IN/translation.json | 1 + src/lib/i18n/locales/pl-PL/translation.json | 1 + src/lib/i18n/locales/pt-BR/translation.json | 1 + src/lib/i18n/locales/pt-PT/translation.json | 1 + src/lib/i18n/locales/ro-RO/translation.json | 1 + src/lib/i18n/locales/ru-RU/translation.json | 1 + src/lib/i18n/locales/sk-SK/translation.json | 1 + src/lib/i18n/locales/sr-RS/translation.json | 1 + src/lib/i18n/locales/sv-SE/translation.json | 1 + src/lib/i18n/locales/ta-IN/translation.json | 1 + src/lib/i18n/locales/th-TH/translation.json | 1 + src/lib/i18n/locales/tk-TM/translation.json | 1 + src/lib/i18n/locales/tr-TR/translation.json | 1 + src/lib/i18n/locales/ug-CN/translation.json | 1 + src/lib/i18n/locales/uk-UA/translation.json | 1 + src/lib/i18n/locales/ur-PK/translation.json | 1 + src/lib/i18n/locales/uz-Cyrl-UZ/translation.json | 1 + src/lib/i18n/locales/uz-Latn-Uz/translation.json | 1 + src/lib/i18n/locales/vi-VN/translation.json | 1 + src/lib/i18n/locales/zh-CN/translation.json | 1 + src/lib/i18n/locales/zh-TW/translation.json | 1 + 62 files changed, 62 insertions(+) diff --git a/src/lib/i18n/locales/ar-BH/translation.json b/src/lib/i18n/locales/ar-BH/translation.json index b7a203804d..052ae0985a 100644 --- a/src/lib/i18n/locales/ar-BH/translation.json +++ b/src/lib/i18n/locales/ar-BH/translation.json @@ -288,6 +288,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/ar/translation.json b/src/lib/i18n/locales/ar/translation.json index b7a600a7d0..14d9a3f7b0 100644 --- a/src/lib/i18n/locales/ar/translation.json +++ b/src/lib/i18n/locales/ar/translation.json @@ -288,6 +288,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "مكالمة", "Call feature is not supported when using Web STT engine": "ميزة الاتصال غير مدعومة عند استخدام محرك Web STT", "Camera": "الكاميرا", diff --git a/src/lib/i18n/locales/az-AZ/translation.json b/src/lib/i18n/locales/az-AZ/translation.json index 55da6b324a..a34c0d9d9e 100644 --- a/src/lib/i18n/locales/az-AZ/translation.json +++ b/src/lib/i18n/locales/az-AZ/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Zəng", "Call feature is not supported when using Web STT engine": "Veb STT mühərriki istifadə edildikdə zəng funksiyası dəstəklənmir", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/bg-BG/translation.json b/src/lib/i18n/locales/bg-BG/translation.json index a42ea8becb..e2d48c0949 100644 --- a/src/lib/i18n/locales/bg-BG/translation.json +++ b/src/lib/i18n/locales/bg-BG/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Обаждане", "Call feature is not supported when using Web STT engine": "Функцията за обаждане не се поддържа при използването на Web STT двигател", "Camera": "Камера", diff --git a/src/lib/i18n/locales/bn-BD/translation.json b/src/lib/i18n/locales/bn-BD/translation.json index 8479ceb451..f8b67a553f 100644 --- a/src/lib/i18n/locales/bn-BD/translation.json +++ b/src/lib/i18n/locales/bn-BD/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/bo-TB/translation.json b/src/lib/i18n/locales/bo-TB/translation.json index 15feef6379..46cd53a802 100644 --- a/src/lib/i18n/locales/bo-TB/translation.json +++ b/src/lib/i18n/locales/bo-TB/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "སྐད་འབོད།", "Call feature is not supported when using Web STT engine": "Web STT མ་ལག་སྤྱོད་སྐབས་སྐད་འབོད་ཀྱི་ཁྱད་ཆོས་ལ་རྒྱབ་སྐྱོར་མེད།", "Camera": "པར་ཆས།", diff --git a/src/lib/i18n/locales/bs-BA/translation.json b/src/lib/i18n/locales/bs-BA/translation.json index f53b3bb1b7..f62d8322b8 100644 --- a/src/lib/i18n/locales/bs-BA/translation.json +++ b/src/lib/i18n/locales/bs-BA/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Poziv", "Call feature is not supported when using Web STT engine": "Značajka poziva nije podržana kada se koristi Web STT mehanizam", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/ca-ES/translation.json b/src/lib/i18n/locales/ca-ES/translation.json index d1d8c1fee9..b6ad44cb17 100644 --- a/src/lib/i18n/locales/ca-ES/translation.json +++ b/src/lib/i18n/locales/ca-ES/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "Calendari eliminat", "Calendar name": "", "Calendars": "Calendaris", + "Calendars Public Sharing": "", "Call": "Trucada", "Call feature is not supported when using Web STT engine": "La funció de trucada no s'admet quan s'utilitza el motor Web STT", "Camera": "Càmera", diff --git a/src/lib/i18n/locales/ceb-PH/translation.json b/src/lib/i18n/locales/ceb-PH/translation.json index 7b14da126c..1e9ecb5e61 100644 --- a/src/lib/i18n/locales/ceb-PH/translation.json +++ b/src/lib/i18n/locales/ceb-PH/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/cs-CZ/translation.json b/src/lib/i18n/locales/cs-CZ/translation.json index 82f937e560..a1cc8e9001 100644 --- a/src/lib/i18n/locales/cs-CZ/translation.json +++ b/src/lib/i18n/locales/cs-CZ/translation.json @@ -286,6 +286,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Volání", "Call feature is not supported when using Web STT engine": "Funkce volání není podporována při použití webového STT jádra.", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/da-DK/translation.json b/src/lib/i18n/locales/da-DK/translation.json index 8fab83db74..a8b5005a86 100644 --- a/src/lib/i18n/locales/da-DK/translation.json +++ b/src/lib/i18n/locales/da-DK/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Opkald", "Call feature is not supported when using Web STT engine": "Opkaldsfunktion er ikke understøttet for Web STT engine", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/de-DE/translation.json b/src/lib/i18n/locales/de-DE/translation.json index 3e6dc991ce..f2dd6de67d 100644 --- a/src/lib/i18n/locales/de-DE/translation.json +++ b/src/lib/i18n/locales/de-DE/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Anruf", "Call feature is not supported when using Web STT engine": "Die Anruffunktion wird bei Verwendung der Web-STT-Engine nicht unterstützt.", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/dg-DG/translation.json b/src/lib/i18n/locales/dg-DG/translation.json index b1bd108a4a..b8c3d02b56 100644 --- a/src/lib/i18n/locales/dg-DG/translation.json +++ b/src/lib/i18n/locales/dg-DG/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/el-GR/translation.json b/src/lib/i18n/locales/el-GR/translation.json index 5657253c95..e2ce062196 100644 --- a/src/lib/i18n/locales/el-GR/translation.json +++ b/src/lib/i18n/locales/el-GR/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Κλήση", "Call feature is not supported when using Web STT engine": "Η λειτουργία κλήσης δεν υποστηρίζεται όταν χρησιμοποιείται η μηχανή Web STT", "Camera": "Κάμερα", diff --git a/src/lib/i18n/locales/en-GB/translation.json b/src/lib/i18n/locales/en-GB/translation.json index 3c57bae694..340c2f2af2 100644 --- a/src/lib/i18n/locales/en-GB/translation.json +++ b/src/lib/i18n/locales/en-GB/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index b41abd4a5b..e3a291a956 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/es-ES/translation.json b/src/lib/i18n/locales/es-ES/translation.json index b289b34e19..2519d4c441 100644 --- a/src/lib/i18n/locales/es-ES/translation.json +++ b/src/lib/i18n/locales/es-ES/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Llamada", "Call feature is not supported when using Web STT engine": "La funcionalidad de Llamada no está soportada cuando se usa el motor Web STT", "Camera": "Cámara", diff --git a/src/lib/i18n/locales/et-EE/translation.json b/src/lib/i18n/locales/et-EE/translation.json index 9808ab1b9e..218c3374b8 100644 --- a/src/lib/i18n/locales/et-EE/translation.json +++ b/src/lib/i18n/locales/et-EE/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Kõne", "Call feature is not supported when using Web STT engine": "Kõnefunktsioon ei ole Web STT mootorit kasutades toetatud", "Camera": "Kaamera", diff --git a/src/lib/i18n/locales/eu-ES/translation.json b/src/lib/i18n/locales/eu-ES/translation.json index b74e7db2ed..f1e6e0d6df 100644 --- a/src/lib/i18n/locales/eu-ES/translation.json +++ b/src/lib/i18n/locales/eu-ES/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Deia", "Call feature is not supported when using Web STT engine": "Dei funtzioa ez da onartzen Web STT motorra erabiltzean", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/fa-IR/translation.json b/src/lib/i18n/locales/fa-IR/translation.json index d9bce2f9ed..20c5ab3cf5 100644 --- a/src/lib/i18n/locales/fa-IR/translation.json +++ b/src/lib/i18n/locales/fa-IR/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "تماس", "Call feature is not supported when using Web STT engine": "ویژگی تماس هنگام استفاده از موتور Web STT پشتیبانی نمی\u200cشود", "Camera": "دوربین", diff --git a/src/lib/i18n/locales/fi-FI/translation.json b/src/lib/i18n/locales/fi-FI/translation.json index 739adcc20e..5dcc8b2693 100644 --- a/src/lib/i18n/locales/fi-FI/translation.json +++ b/src/lib/i18n/locales/fi-FI/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "Kalenteri poistettu", "Calendar name": "", "Calendars": "Kalenterit", + "Calendars Public Sharing": "", "Call": "Puhelu", "Call feature is not supported when using Web STT engine": "Puhelutoimintoa ei tueta käytettäessä web-puheentunnistusmoottoria", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/fil-PH/translation.json b/src/lib/i18n/locales/fil-PH/translation.json index f27acf1909..7af6ea7bc0 100644 --- a/src/lib/i18n/locales/fil-PH/translation.json +++ b/src/lib/i18n/locales/fil-PH/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "Mga Kalendaryo", + "Calendars Public Sharing": "", "Call": "Tawag", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/fr-CA/translation.json b/src/lib/i18n/locales/fr-CA/translation.json index 7eec8292c3..86985cb37f 100644 --- a/src/lib/i18n/locales/fr-CA/translation.json +++ b/src/lib/i18n/locales/fr-CA/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Appeler", "Call feature is not supported when using Web STT engine": "La fonction d'appel n'est pas prise en charge lors de l'utilisation du moteur Web STT", "Camera": "Appareil photo", diff --git a/src/lib/i18n/locales/fr-FR/translation.json b/src/lib/i18n/locales/fr-FR/translation.json index dbf21b5439..cc9ff63852 100644 --- a/src/lib/i18n/locales/fr-FR/translation.json +++ b/src/lib/i18n/locales/fr-FR/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Appeler", "Call feature is not supported when using Web STT engine": "La fonction d'appel n'est pas prise en charge lors de l'utilisation du moteur Web STT", "Camera": "Appareil photo", diff --git a/src/lib/i18n/locales/gl-ES/translation.json b/src/lib/i18n/locales/gl-ES/translation.json index 26fda5db76..d97c6723a9 100644 --- a/src/lib/i18n/locales/gl-ES/translation.json +++ b/src/lib/i18n/locales/gl-ES/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Chamada", "Call feature is not supported when using Web STT engine": "A funcionalidade da chamada non pode usarse xunto co motor da STT Web", "Camera": "Cámara", diff --git a/src/lib/i18n/locales/he-IL/translation.json b/src/lib/i18n/locales/he-IL/translation.json index b8e9ac0f6b..158bbbf647 100644 --- a/src/lib/i18n/locales/he-IL/translation.json +++ b/src/lib/i18n/locales/he-IL/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "מצלמה", diff --git a/src/lib/i18n/locales/hi-IN/translation.json b/src/lib/i18n/locales/hi-IN/translation.json index 43fb69c517..29c6231d49 100644 --- a/src/lib/i18n/locales/hi-IN/translation.json +++ b/src/lib/i18n/locales/hi-IN/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/hr-HR/translation.json b/src/lib/i18n/locales/hr-HR/translation.json index 8067fecbe1..1debce8c4a 100644 --- a/src/lib/i18n/locales/hr-HR/translation.json +++ b/src/lib/i18n/locales/hr-HR/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Poziv", "Call feature is not supported when using Web STT engine": "Značajka poziva nije podržana kada se koristi Web STT mehanizam", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/hu-HU/translation.json b/src/lib/i18n/locales/hu-HU/translation.json index 2a5ff48a2e..c5225f4a5c 100644 --- a/src/lib/i18n/locales/hu-HU/translation.json +++ b/src/lib/i18n/locales/hu-HU/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Hívás", "Call feature is not supported when using Web STT engine": "A hívás funkció nem támogatott Web STT motor használatakor", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/id-ID/translation.json b/src/lib/i18n/locales/id-ID/translation.json index ec24db30ea..00b0e11322 100644 --- a/src/lib/i18n/locales/id-ID/translation.json +++ b/src/lib/i18n/locales/id-ID/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Panggilan", "Call feature is not supported when using Web STT engine": "Fitur panggilan tidak didukung saat menggunakan mesin Web STT", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/ie-GA/translation.json b/src/lib/i18n/locales/ie-GA/translation.json index 738798b6de..1e4d676677 100644 --- a/src/lib/i18n/locales/ie-GA/translation.json +++ b/src/lib/i18n/locales/ie-GA/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Glaoigh", "Call feature is not supported when using Web STT engine": "Ní thacaítear le gné glaonna agus inneall Web STT á úsáid", "Camera": "Ceamara", diff --git a/src/lib/i18n/locales/it-IT/translation.json b/src/lib/i18n/locales/it-IT/translation.json index 07bc371719..b561d8b044 100644 --- a/src/lib/i18n/locales/it-IT/translation.json +++ b/src/lib/i18n/locales/it-IT/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Chiamata", "Call feature is not supported when using Web STT engine": "La funzione di chiamata non è supportata quando si utilizza il motore Web STT", "Camera": "Fotocamera", diff --git a/src/lib/i18n/locales/ja-JP/translation.json b/src/lib/i18n/locales/ja-JP/translation.json index 43781073b2..5834e24c23 100644 --- a/src/lib/i18n/locales/ja-JP/translation.json +++ b/src/lib/i18n/locales/ja-JP/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "コール", "Call feature is not supported when using Web STT engine": "Web STTエンジンを使用している場合、コール機能は使用できません", "Camera": "カメラ", diff --git a/src/lib/i18n/locales/ka-GE/translation.json b/src/lib/i18n/locales/ka-GE/translation.json index c5a4ee896d..1f4562129f 100644 --- a/src/lib/i18n/locales/ka-GE/translation.json +++ b/src/lib/i18n/locales/ka-GE/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "ზარი", "Call feature is not supported when using Web STT engine": "", "Camera": "კამერა", diff --git a/src/lib/i18n/locales/kab-DZ/translation.json b/src/lib/i18n/locales/kab-DZ/translation.json index 9e97773ab7..67ac4da3c2 100644 --- a/src/lib/i18n/locales/kab-DZ/translation.json +++ b/src/lib/i18n/locales/kab-DZ/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Siwel", "Call feature is not supported when using Web STT engine": "Tamahilt n usiwel ur tettwasefrak ara mi ara tesqedceḍ amsedday Web STT", "Camera": "Takamiṛatt", diff --git a/src/lib/i18n/locales/ko-KR/translation.json b/src/lib/i18n/locales/ko-KR/translation.json index 5454d40591..d2583ca02c 100644 --- a/src/lib/i18n/locales/ko-KR/translation.json +++ b/src/lib/i18n/locales/ko-KR/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/lt-LT/translation.json b/src/lib/i18n/locales/lt-LT/translation.json index 0f2cdbf358..966cdf9c74 100644 --- a/src/lib/i18n/locales/lt-LT/translation.json +++ b/src/lib/i18n/locales/lt-LT/translation.json @@ -286,6 +286,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Skambinti", "Call feature is not supported when using Web STT engine": "Skambučio funkcionalumas neleidžiamas naudojant Web STT variklį", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/lv-LV/translation.json b/src/lib/i18n/locales/lv-LV/translation.json index 9a088f7d9b..062d67f052 100644 --- a/src/lib/i18n/locales/lv-LV/translation.json +++ b/src/lib/i18n/locales/lv-LV/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Zvans", "Call feature is not supported when using Web STT engine": "Zvana funkcija nav atbalstīta, izmantojot Web STT dzinēju", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/ms-MY/translation.json b/src/lib/i18n/locales/ms-MY/translation.json index 4b1cfcbb8d..d4fe21b6d4 100644 --- a/src/lib/i18n/locales/ms-MY/translation.json +++ b/src/lib/i18n/locales/ms-MY/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Hubungi", "Call feature is not supported when using Web STT engine": "Ciri panggilan tidak disokong apabila menggunakan enjin Web STT", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/nb-NO/translation.json b/src/lib/i18n/locales/nb-NO/translation.json index 3872effb77..af958eaf04 100644 --- a/src/lib/i18n/locales/nb-NO/translation.json +++ b/src/lib/i18n/locales/nb-NO/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Ring", "Call feature is not supported when using Web STT engine": "Ringefunksjonen støttes ikke når du bruker Web STT-motoren", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/nl-NL/translation.json b/src/lib/i18n/locales/nl-NL/translation.json index 90f075797b..88927413c3 100644 --- a/src/lib/i18n/locales/nl-NL/translation.json +++ b/src/lib/i18n/locales/nl-NL/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "Agenda verwijderd", "Calendar name": "", "Calendars": "Agenda's", + "Calendars Public Sharing": "", "Call": "Oproep", "Call feature is not supported when using Web STT engine": "Belfunctie wordt niet ondersteund bij gebruik van de Web STT engine", "Camera": "Camera", diff --git a/src/lib/i18n/locales/pa-IN/translation.json b/src/lib/i18n/locales/pa-IN/translation.json index e16c3eb0cc..7a038e52f2 100644 --- a/src/lib/i18n/locales/pa-IN/translation.json +++ b/src/lib/i18n/locales/pa-IN/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/pl-PL/translation.json b/src/lib/i18n/locales/pl-PL/translation.json index 0541c7140f..010518f5bb 100644 --- a/src/lib/i18n/locales/pl-PL/translation.json +++ b/src/lib/i18n/locales/pl-PL/translation.json @@ -286,6 +286,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Rozmowa", "Call feature is not supported when using Web STT engine": "Funkcja rozmowy nie jest obsługiwana przy użyciu przeglądarkowego silnika STT", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json index f1ab564cf0..22ef167943 100644 --- a/src/lib/i18n/locales/pt-BR/translation.json +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "Calendário excluído", "Calendar name": "", "Calendars": "Calendários", + "Calendars Public Sharing": "", "Call": "Chamada", "Call feature is not supported when using Web STT engine": "O recurso de chamada não é suportado ao usar o mecanismo Web STT", "Camera": "Câmera", diff --git a/src/lib/i18n/locales/pt-PT/translation.json b/src/lib/i18n/locales/pt-PT/translation.json index 1c9db6a2fd..d48ac1baa7 100644 --- a/src/lib/i18n/locales/pt-PT/translation.json +++ b/src/lib/i18n/locales/pt-PT/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Chamar", "Call feature is not supported when using Web STT engine": "A funcionalide de Chamar não é suportada quando usa um motor Web STT", "Camera": "Câmara", diff --git a/src/lib/i18n/locales/ro-RO/translation.json b/src/lib/i18n/locales/ro-RO/translation.json index 802368b635..ce36178dfa 100644 --- a/src/lib/i18n/locales/ro-RO/translation.json +++ b/src/lib/i18n/locales/ro-RO/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Apel", "Call feature is not supported when using Web STT engine": "Funcția de apel nu este suportată când se utilizează motorul Web STT", "Camera": "Cameră", diff --git a/src/lib/i18n/locales/ru-RU/translation.json b/src/lib/i18n/locales/ru-RU/translation.json index 2b6c805709..461baa20cf 100644 --- a/src/lib/i18n/locales/ru-RU/translation.json +++ b/src/lib/i18n/locales/ru-RU/translation.json @@ -286,6 +286,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Вызов", "Call feature is not supported when using Web STT engine": "Функция вызова не поддерживается при использовании Web STT (распознавание речи) движка", "Camera": "Камера", diff --git a/src/lib/i18n/locales/sk-SK/translation.json b/src/lib/i18n/locales/sk-SK/translation.json index aaa9d8807c..f0165bcbfa 100644 --- a/src/lib/i18n/locales/sk-SK/translation.json +++ b/src/lib/i18n/locales/sk-SK/translation.json @@ -286,6 +286,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Volanie", "Call feature is not supported when using Web STT engine": "Funkcia volania nie je podporovaná pri použití Web STT engine.", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/sr-RS/translation.json b/src/lib/i18n/locales/sr-RS/translation.json index 9d2c8fd3b5..ea4d1dc188 100644 --- a/src/lib/i18n/locales/sr-RS/translation.json +++ b/src/lib/i18n/locales/sr-RS/translation.json @@ -285,6 +285,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Позив", "Call feature is not supported when using Web STT engine": "", "Camera": "Камера", diff --git a/src/lib/i18n/locales/sv-SE/translation.json b/src/lib/i18n/locales/sv-SE/translation.json index dca3ed2db1..71c740212f 100644 --- a/src/lib/i18n/locales/sv-SE/translation.json +++ b/src/lib/i18n/locales/sv-SE/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Samtal", "Call feature is not supported when using Web STT engine": "Samtalsfunktionen är inte kompatibel med Web Tal-till-text motor", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/ta-IN/translation.json b/src/lib/i18n/locales/ta-IN/translation.json index 36a86d77d6..27c575e00f 100644 --- a/src/lib/i18n/locales/ta-IN/translation.json +++ b/src/lib/i18n/locales/ta-IN/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "அழைக்கவும்", "Call feature is not supported when using Web STT engine": "Web STT இன்ஜினைப் பயன்படுத்தும் போது அழைப்பு அம்சம் ஆதரிக்கப்படாது", "Camera": "கேமரா", diff --git a/src/lib/i18n/locales/th-TH/translation.json b/src/lib/i18n/locales/th-TH/translation.json index abae0a891c..872ef186e0 100644 --- a/src/lib/i18n/locales/th-TH/translation.json +++ b/src/lib/i18n/locales/th-TH/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "โทร", "Call feature is not supported when using Web STT engine": "ไม่รองรับฟีเจอร์การโทรเมื่อใช้เอนจิน Web STT", "Camera": "กล้อง", diff --git a/src/lib/i18n/locales/tk-TM/translation.json b/src/lib/i18n/locales/tk-TM/translation.json index c327e83df2..522194374a 100644 --- a/src/lib/i18n/locales/tk-TM/translation.json +++ b/src/lib/i18n/locales/tk-TM/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "", "Call feature is not supported when using Web STT engine": "", "Camera": "", diff --git a/src/lib/i18n/locales/tr-TR/translation.json b/src/lib/i18n/locales/tr-TR/translation.json index 970a0dbd59..369d4f3a7a 100644 --- a/src/lib/i18n/locales/tr-TR/translation.json +++ b/src/lib/i18n/locales/tr-TR/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Arama", "Call feature is not supported when using Web STT engine": "Web STT motoru kullanılırken arama özelliği desteklenmiyor", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/ug-CN/translation.json b/src/lib/i18n/locales/ug-CN/translation.json index 2fe7eed31a..24d213ff66 100644 --- a/src/lib/i18n/locales/ug-CN/translation.json +++ b/src/lib/i18n/locales/ug-CN/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "چاقىرىش", "Call feature is not supported when using Web STT engine": "تور STT ماتورى ئىشلىتىلگەندە چاقىرىش ئىقتىدارى قوللىنىلمايدۇ", "Camera": "كامېرا", diff --git a/src/lib/i18n/locales/uk-UA/translation.json b/src/lib/i18n/locales/uk-UA/translation.json index 962e5bd3ee..6efe252dbe 100644 --- a/src/lib/i18n/locales/uk-UA/translation.json +++ b/src/lib/i18n/locales/uk-UA/translation.json @@ -286,6 +286,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Виклик", "Call feature is not supported when using Web STT engine": "Функція виклику не підтримується при використанні Web STT (розпізнавання мовлення) рушія", "Camera": "Камера", diff --git a/src/lib/i18n/locales/ur-PK/translation.json b/src/lib/i18n/locales/ur-PK/translation.json index 6265734fa5..c549638827 100644 --- a/src/lib/i18n/locales/ur-PK/translation.json +++ b/src/lib/i18n/locales/ur-PK/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "کال کریں", "Call feature is not supported when using Web STT engine": "کال کی خصوصیت ویب STT انجن استعمال کرتے وقت معاونت یافتہ نہیں ہے", "Camera": "کیمرہ", diff --git a/src/lib/i18n/locales/uz-Cyrl-UZ/translation.json b/src/lib/i18n/locales/uz-Cyrl-UZ/translation.json index 45461121bf..14d3ff6eec 100644 --- a/src/lib/i18n/locales/uz-Cyrl-UZ/translation.json +++ b/src/lib/i18n/locales/uz-Cyrl-UZ/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Қўнғироқ қилинг", "Call feature is not supported when using Web STT engine": "Wеб СТТ механизмидан фойдаланилганда қўнғироқ функсияси қўллаб-қувватланмайди", "Camera": "Камера", diff --git a/src/lib/i18n/locales/uz-Latn-Uz/translation.json b/src/lib/i18n/locales/uz-Latn-Uz/translation.json index 7eb4c95f36..a48bc65b8f 100644 --- a/src/lib/i18n/locales/uz-Latn-Uz/translation.json +++ b/src/lib/i18n/locales/uz-Latn-Uz/translation.json @@ -284,6 +284,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Qo'ng'iroq qiling", "Call feature is not supported when using Web STT engine": "Web STT mexanizmidan foydalanilganda qo'ng'iroq funksiyasi qo'llab-quvvatlanmaydi", "Camera": "Kamera", diff --git a/src/lib/i18n/locales/vi-VN/translation.json b/src/lib/i18n/locales/vi-VN/translation.json index 0df9c01f87..d9d62307c4 100644 --- a/src/lib/i18n/locales/vi-VN/translation.json +++ b/src/lib/i18n/locales/vi-VN/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "", "Calendar name": "", "Calendars": "", + "Calendars Public Sharing": "", "Call": "Gọi", "Call feature is not supported when using Web STT engine": "Tính năng gọi điện không được hỗ trợ khi sử dụng công cụ Web STT", "Camera": "Máy ảnh", diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index 5b52ff8aaa..84ca5b9ce4 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "日历已删除", "Calendar name": "", "Calendars": "日历", + "Calendars Public Sharing": "", "Call": "语音通话", "Call feature is not supported when using Web STT engine": "使用 Web 语音转文字引擎时不支持语音通话功能", "Camera": "摄像头", diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index 3853117546..66ea78ab9b 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -283,6 +283,7 @@ "Calendar deleted": "日曆已刪除", "Calendar name": "", "Calendars": "日曆", + "Calendars Public Sharing": "", "Call": "通話", "Call feature is not supported when using Web STT engine": "使用網頁語音辨識 (Web STT) 引擎時不支援通話功能", "Camera": "相機", From b0a56375d249234a9903de0fa762b9e8ac463118 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 9 May 2026 23:38:32 +0900 Subject: [PATCH 13/37] chore --- .github/pull_request_template.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index daf908c02d..2f44750fe0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,6 +6,16 @@ # Pull Request Checklist +### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) to discuss your idea/fix with the community before creating a pull request, and describe your changes before submitting a pull request. + +This is to ensure large feature PRs are discussed with the community first, before starting work on it. If the community does not want this feature or it is not relevant for Open WebUI as a project, it can be identified in the discussion before working on the feature and submitting the PR. + + + **Before submitting, make sure you've checked the following:** - [ ] **Linked Issue/Discussion:** This PR references an existing [Issue](https://github.com/open-webui/open-webui/issues) or [Discussion](https://github.com/open-webui/open-webui/discussions) — `Closes #___` / `Relates to #___`. If one does not exist, create one first. PRs without a linked issue or discussion may be closed without review. From 5b13e3e3f08172add7a82b71f86663da30d1520a Mon Sep 17 00:00:00 2001 From: joaoback <156559121+joaoback@users.noreply.github.com> Date: Sun, 10 May 2026 12:57:18 -0300 Subject: [PATCH 14/37] i18n: add pt-BR translations for newly added UI items and consistency pass (#24503) 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. --- src/lib/i18n/locales/pt-BR/translation.json | 70 ++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json index 22ef167943..a61f3b2c3f 100644 --- a/src/lib/i18n/locales/pt-BR/translation.json +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -281,11 +281,11 @@ "Bypass Web Loader": "Ignorar carregador da Web", "Cache Base Model List": "Lista de modelos base de cache", "Calendar": "Calendário", - "Calendar created": "", + "Calendar created": "Calendário criado", "Calendar deleted": "Calendário excluído", - "Calendar name": "", + "Calendar name": "Nome do calendário", "Calendars": "Calendários", - "Calendars Public Sharing": "", + "Calendars Public Sharing": "Compartilhamento Público de Calendários", "Call": "Chamada", "Call feature is not supported when using Web STT engine": "O recurso de chamada não é suportado ao usar o mecanismo Web STT", "Camera": "Câmera", @@ -317,7 +317,7 @@ "Chat Bubble UI": "Interface de Bolha de Chat", "Chat Completions": "Gerar Resposta", "Chat Conversation": "Conversa do Chat", - "Chat deleted.": "", + "Chat deleted.": "Chat excluído.", "Chat direction": "Direção do Chat", "Chat exported successfully": "Chat exportado com sucesso", "Chat History": "Histórico de chat", @@ -328,7 +328,7 @@ "Chat unshared successfully.": "Compartilhamento do chat removido com sucesso.", "chats": "chats", "Chats": "Chats", - "Chats Public Sharing": "", + "Chats Public Sharing": "Compartilhamento Público de Chats", "Check Again": "Verificar Novamente", "Check for updates": "Verificar atualizações", "Checking for updates...": "Verificando atualizações...", @@ -435,7 +435,7 @@ "Content": "Conteúdo", "Content Extraction Engine": "Mecanismo de Extração de Conteúdo", "Content lengths (character counts only)": "Extensão do conteúdo (apenas em caracteres)", - "Context Tokens": "", + "Context Tokens": "Tokens de Contexto", "Continue Response": "Continuar Resposta", "Continue with {{provider}}": "Continuar com {{provider}}", "Continue with Email": "Continuar com Email", @@ -495,7 +495,7 @@ "Current Model": "Modelo Atual", "Current Password": "Senha Atual", "Custom": "Personalizado", - "Custom color": "", + "Custom color": "Cor personalizada", "Custom description enabled": "Descrição personalizada habilitada", "Custom Gender": "Gênero personalizado", "Custom Parameter Name": "Nome do parâmetro personalizado", @@ -507,7 +507,7 @@ "Data Controls": "Controle de Dados", "Database": "Banco de Dados", "Datalab Marker API": "API do Marcador do Datalab", - "Date Modified": "", + "Date Modified": "Data de Modificação", "Day": "Dia", "DD/MM/YYYY": "DD/MM/AAAA", "DDGS Backend": "Backend DDGS", @@ -586,7 +586,7 @@ "Disable Image Extraction": "Desativar extração de imagem", "Disable image extraction from the PDF. If Use LLM is enabled, images will be automatically captioned. Defaults to False.": "Desabilite a extração de imagens do PDF. Se a opção Usar LLM estiver habilitada, as imagens serão legendadas automaticamente. O padrão é Falso.", "Disabled": "Desativado", - "Disconnect OAuth": "", + "Disconnect OAuth": "Desconectar OAuth", "Discover a function": "Descubra uma função", "Discover a model": "Descubra um modelo", "Discover a prompt": "Descubra um prompt", @@ -776,8 +776,8 @@ "Enter New Password": "Digite uma nova senha", "Enter Number of Steps (e.g. 50)": "Digite o Número de Passos (por exemplo, 50)", "Enter Ollama Cloud API Key": "Insira a chave da API do Ollama Cloud", - "Enter PaddleOCR-vl API Base URL": "", - "Enter PaddleOCR-vl API Token": "", + "Enter PaddleOCR-vl API Base URL": "Insira a URL base da API PaddleOCR-vl", + "Enter PaddleOCR-vl API Token": "Insira o token da API PaddleOCR-vl", "Enter Perplexity API Key": "Insira a chave da API Perplexity", "Enter Perplexity Search API URL": "Insira a URL da API de pesquisa Perplexity", "Enter Playwright Timeout": "Insira o tempo limite do Playwright", @@ -908,7 +908,7 @@ "Failed to create API Key.": "Falha ao criar a Chave API.", "Failed to delete calendar": "Falha ao excluir calendário", "Failed to delete note": "Falha ao excluir a nota", - "Failed to disconnect": "", + "Failed to disconnect": "Falha ao desconectar", "Failed to download image": "Falha ao baixar a imagem", "Failed to extract content from the file: {{error}}": "Falha ao extrair conteúdo do arquivo: {{error}}", "Failed to extract content from the file.": "Falha ao extrair conteúdo do arquivo.", @@ -1232,10 +1232,10 @@ "List calendars, search, create, update, and delete calendar events": "Listar calendários, pesquisar, criar, atualizar e excluir eventos do calendário", "Listening...": "Escutando...", "Live": "Ao vivo", - "llama.cpp": "", + "llama.cpp": "llama.cpp", "Llama.cpp": "Llama.cpp", "LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.", - "Loaded": "", + "Loaded": "Carregado", "Loader": "Carregador", "Loading Kokoro.js...": "Carregando Kokoro.js...", "Loading...": "Carregando...", @@ -1266,7 +1266,7 @@ "Markdown": "Markdown", "Markdown Header Text Splitter": "Separador de texto de cabeçalho Markdown", "Max Speakers": "Máximo de locutores", - "Max tokens to retrieve (1024-32768, default 8192)": "", + "Max tokens to retrieve (1024-32768, default 8192)": "Máximo de tokens para recuperar (1024-32768, padrão 8192)", "Max Upload Count": "Quantidade máxima de anexos", "Max Upload Size": "Tamanho máximo do arquivo", "Maximum characters to return from fetched URLs. Leave empty for no limit.": "Número máximo de caracteres a retornar de URLs buscadas. Deixe em branco para sem limite.", @@ -1295,7 +1295,7 @@ "Message counts and response timestamps": "Contagem de mensagens e registros de data e hora de resposta", "Message counts are based on assistant responses.": "A contagem de mensagens é baseada nas respostas do assistente.", "Message rating should be enabled to use this feature": "A avaliação de mensagens deve estar habilitada para usar este recurso", - "Message text...": "", + "Message text...": "Texto da mensagem...", "messages": "mensagens", "Messages": "Mensagens", "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Mensagens enviadas após criar seu link não serão compartilhadas. Usuários com o URL poderão visualizar o chat compartilhado.", @@ -1362,12 +1362,12 @@ "More Options": "Mais opções", "Move": "Mover", "Moved {{name}}": "{{name}} movido", - "Mute": "", - "Muted": "", + "Mute": "Silenciar", + "Muted": "Silenciado", "My Terminal": "Meu Terminal", "Name": "Nome", "Name and ID are required, please fill them out": "Nome e ID são obrigatórios, por favor preencha-os", - "Name is required": "", + "Name is required": "O nome é obrigatório", "Name your knowledge base": "Nome da sua base de conhecimento", "Name, prompt, and model are required": "Nome, prompt e modelo são obrigatórios.", "Native": "Nativo", @@ -1375,8 +1375,8 @@ "New": "Novo", "New Automation": "Nova Automação", "New Button": "Novo Botão", - "New calendar": "", - "New Calendar": "", + "New calendar": "Novo calendário", + "New Calendar": "Novo Calendário", "New Chat": "Novo Chat", "New Event": "Novo Evento", "New File": "Novo Arquivo", @@ -1434,7 +1434,7 @@ "No Notes": "Sem Notas", "No notes found": "Notas não encontradas", "No one": "Ninguém", - "No output items": "", + "No output items": "Nenhum item de saída", "No pinned messages": "Nenhuma mensagem fixada", "No prompts found": "Nenhum prompt encontrado", "No results": "Nenhum resultado encontrado", @@ -1473,8 +1473,8 @@ "OAuth 2.1": "OAuth 2.1", "OAuth 2.1 (Static)": "OAuth 2.1 (Estático)", "OAuth ID": "OAuth ID", - "OAuth Server URL": "", - "OAuth session disconnected": "", + "OAuth Server URL": "URL do Servidor OAuth", + "OAuth session disconnected": "Sessão OAuth desconectada", "October": "Outubro", "Off": "Desligado", "Okay, Let's Go!": "Ok, Vamos Lá!", @@ -1541,8 +1541,8 @@ "Output format": "Formato de saída", "Output Format": "Formato de Saída", "Overview": "Visão Geral", - "PaddleOCR-vl": "", - "PaddleOCR-vl API URL required.": "", + "PaddleOCR-vl": "PaddleOCR-vl", + "PaddleOCR-vl API URL required.": "URL da API PaddleOCR-vl é obrigatória.", "page": "página", "Page": "Página", "Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.": "O modo de página cria um documento por página. O modo único combina todas as páginas em um único documento para melhor divisão entre páginas.", @@ -1632,13 +1632,13 @@ "Prompt created successfully": "Prompt criado com sucesso", "Prompt Name": "Nome do prompt", "Prompt Suggestions": "Sugestões de Prompt", - "Prompt Template": "", + "Prompt Template": "Modelo de Prompt", "Prompt updated successfully": "Prompt atualizado com sucesso", "Prompts": "Prompts", "Prompts Access": "Acesso aos Prompts", "Prompts Public Sharing": "Compartilhamento Público dos Prompts", "Prompts Sharing": "Compartilhamento de Prompts", - "Provider": "", + "Provider": "Provedor", "Public": "Público", "Pull \"{{searchValue}}\" from Ollama.com": "Obter \"{{searchValue}}\" de Ollama.com", "Pull a model from Ollama.com": "Obter um modelo de Ollama.com", @@ -1661,7 +1661,7 @@ "Reason": "Razão", "Reasoning Effort": "Esforço de raciocínio", "Reasoning Tags": "Tags de raciocínio", - "Reasoning text...": "", + "Reasoning text...": "Texto de raciocínio...", "Recently Used": "Usado recentemente", "Reconnected": "Reconectado", "Record": "Gravar", @@ -1751,7 +1751,7 @@ "Schedule": "Agendar", "Scheduled time must be in the future": "O horário agendado deve ser no futuro.", "Scroll On Branch Change": "Rolar na mudança de ramo", - "Scroll to Top": "", + "Scroll to Top": "Rolar para o topo", "Search": "Pesquisar", "Search a model": "Pesquisar um modelo", "Search all emojis": "Pesquisar todos os emojis", @@ -1975,8 +1975,8 @@ "Support": "Suporte", "Support this plugin:": "Apoie este plugin:", "Supported MIME Types": "Tipos MIME suportados", - "Switch to JSON editor": "", - "Switch to visual editor": "", + "Switch to JSON editor": "Mudar para editor JSON", + "Switch to visual editor": "Mudar para editor visual", "Sync": "Sincronizar", "Sync Complete!": "Sincronização concluída!", "Sync directory": "Sincronizar Diretório", @@ -2052,7 +2052,7 @@ "This option sets the maximum number of tokens the model can generate in its response. Increasing this limit allows the model to provide longer answers, but it may also increase the likelihood of unhelpful or irrelevant content being generated.": "Esta opção define o número máximo de tokens que o modelo pode gerar em sua resposta. Aumentar esse limite permite que o modelo forneça respostas mais longas, mas também pode aumentar a probabilidade de geração de conteúdo inútil ou irrelevante.", "This option will delete all existing files in the collection and replace them with newly uploaded files.": "Essa opção deletará todos os arquivos existentes na coleção e todos eles serão substituídos.", "This response was generated by \"{{model}}\"": "Esta resposta foi gerada por \"{{model}}\"", - "This template contains multiple context placeholders ([context] or {{CONTEXT}}). Context will be injected at each occurrence.": "", + "This template contains multiple context placeholders ([context] or {{CONTEXT}}). Context will be injected at each occurrence.": "Este template contém múltiplos marcadores de contexto ([context] ou {{CONTEXT}}). O contexto será injetado em cada ocorrência.", "This will delete": "Isso vai excluir", "This will delete {{NAME}} and all its contents.": "Esta ação excluirá {{NAME}} e todos seus conteúdos.", "This will delete all models including custom models": "Isto vai excluir todos os modelos, incluindo personalizados", @@ -2143,7 +2143,7 @@ "Unknown User": "Usuário desconhecido", "Unloads {{FROM_NOW}}": "Descarrega {{FROM_NOW}}", "Unlock mysteries": "Desvendar mistérios", - "Unmute": "", + "Unmute": "Reativar som", "Unpin": "Desfixar", "Unpin from Sidebar": "Desfixar da barra lateral", "Unravel secrets": "Desvendar segredos", @@ -2222,7 +2222,7 @@ "Visible": "Visível", "Visible to all users": "Visível para todos os usuários", "Vision": "Visão", - "Visual": "", + "Visual": "Visual", "Voice": "Voz", "Voice Input": "Entrada de voz", "Voice mode": "Modo de voz", From e7ba8978c68b672a9ef44b72091ff3ff59f098b5 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 17:57:48 +0200 Subject: [PATCH 15/37] fix: reject parser-confusing chars in validate_url to close SSRF bypass (#24534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit urllib.parse.urlparse and requests/aiohttp disagree on how to split URLs containing backslash, tab, CR, or LF in or around the netloc. urlparse treats backslash as part of userinfo and uses what follows '@' as the host; requests treats backslash as the start of the path and connects to whatever precedes it. The same URL therefore passes the private-IP filter (urlparse sees a public host) but reaches an internal target (requests connects to e.g. 127.0.0.1). End result is an SSRF that the existing IP block list cannot catch because it's evaluating the wrong host. PoC: http://127.0.0.1:6666\@1.1.1.1 — urlparse hostname is 1.1.1.1 (global, passes), requests reaches 127.0.0.1 (loopback). Reject up front any URL containing one of the four documented parser- confusing characters before either parser gets a chance to interpret it. None of these characters is valid in an unencoded URL (\ should always be %5C, whitespace should be %09 / %0A / %0D), so this is a pure defensive rejection with no legitimate-input false positives. Reported by Fushuling and RacerZ-fighting in GHSA-8w7q-q5jp-jvgx. Co-authored-by: Fushuling Co-authored-by: RacerZ-fighting --- backend/open_webui/retrieval/web/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/open_webui/retrieval/web/utils.py b/backend/open_webui/retrieval/web/utils.py index 633f4bba5e..c2ce6bdbd1 100644 --- a/backend/open_webui/retrieval/web/utils.py +++ b/backend/open_webui/retrieval/web/utils.py @@ -69,6 +69,14 @@ def validate_url(url: Union[str, Sequence[str]]): if isinstance(validators.url(url), validators.ValidationError): raise ValueError(ERROR_MESSAGES.INVALID_URL) + # Reject parser-confusing chars: urlparse and requests/aiohttp split + # on these differently, e.g. http://127.0.0.1\@1.1.1.1 → urlparse + # extracts 1.1.1.1 (public, passes filter) while requests connects + # to 127.0.0.1 (internal). Same shape with tab/CR/LF. + if any(ch in url for ch in ('\\', '\t', '\n', '\r')): + log.warning(f'Blocked URL with parser-confusing char: {url!r}') + raise ValueError(ERROR_MESSAGES.INVALID_URL) + parsed_url = urllib.parse.urlparse(url) # Protocol validation - only allow http/https From c66c273f62a67802ad777cd3b7495cd624867575 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 17:59:08 +0200 Subject: [PATCH 16/37] fix: strip model params for read-only callers on per-id endpoint (#24525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/v1/models/model?id= at routers/models.py:412 returned the full model.model_dump() to any caller with read access, including the params dict that holds the admin-curated system prompt and other behavior config. The user-facing /api/models endpoint already strips this via utils/models.py:170,210 with the comment "Remove params to avoid exposing sensitive info", and /api/v1/models/list gates by write permission so non-curators don't see the model in their workspace listing at all. The per-id endpoint missed the same gate, so a user with read-only access (e.g. granted access to use the model in chat) could open /workspace/models/edit?id= in the browser and read the system prompt verbatim from the network response, even though saving was correctly blocked. Compute write_access once at the top of the handler so it can serve both the response-shape decision and the response field. When the caller lacks write access, replace params with an empty dict in the serialised response. Owners, admins under BYPASS_ADMIN_ACCESS_CONTROL, and explicit write-grant holders still get the full payload so the workspace edit UI keeps working for users who legitimately curate the model. Read-permission users continue to receive everything else they need to chat with the model — the chat path resolves prompt/params server-side from the stored ModelModel and never echoes them back through this endpoint. Reported by destination-one in GHSA-h2cw-7qw9-56xr. Co-authored-by: destination-one --- backend/open_webui/routers/models.py | 38 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 1ced11b358..cef4429760 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -413,9 +413,20 @@ class ModelIdForm(BaseModel): async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): model = await Models.get_model_by_id(id, db=db) if model: - if ( + write_access = ( (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) - or model.user_id == user.id + or user.id == model.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + ) + + if ( + write_access or await AccessGrants.has_access( user_id=user.id, resource_type='model', @@ -424,19 +435,18 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes db=db, ) ): + model_dict = model.model_dump() + # Strip params (system prompt and other admin-curated config) + # for read-only callers — matches the params strip already + # enforced on /api/models in utils/models.py. Owners, admins + # under BYPASS_ADMIN_ACCESS_CONTROL, and write-grant holders + # still receive the full object so the workspace edit UI keeps + # working for users who legitimately curate the model. + if not write_access: + model_dict['params'] = {} return ModelAccessResponse( - **model.model_dump(), - write_access=( - (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) - or user.id == model.user_id - or await AccessGrants.has_access( - user_id=user.id, - resource_type='model', - resource_id=model.id, - permission='write', - db=db, - ) - ), + **model_dict, + write_access=write_access, ) else: raise HTTPException( From 2d9939ed4964bb9e5b80018f1739708fc4e55092 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 17:59:32 +0200 Subject: [PATCH 17/37] chore: add validate_url() to get_image_data() for cohort consistency hardening (#24518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add validate_url() to get_image_data() for cohort consistency hardening `get_image_data()` in `backend/open_webui/routers/images.py` fetches the URL returned by the configured image generation API directly via `session.get(data)` without first calling `validate_url()`. The sibling `load_url_image()` in the same file (called from /images/edit) calls `validate_url(data)` first — that gate was added under GHSA-jgx9-jr5x-mvpv. The two functions handle structurally identical input (an attacker-or-server-supplied URL string) and should enforce the same SSRF gate as a matter of code hygiene. In the current call graph, the URL passed to `get_image_data()` comes from the admin-configured image generation API's response, so an exploitable SSRF chain additionally requires admin-side trust delegation (misconfigured/untrusted upstream image API, or a custom OpenAI-compatible server that reflects user input into response URLs). That makes the missing call a defense-in-depth gap rather than a vulnerability per SECURITY.md Rule 9 — a position the GHSA-h7cc-wwjp-5xqh advisory is being closed under. This change is hardening: it brings the two image-fetch helpers into alignment so any future caller that begins passing user-influenced URLs into `get_image_data()` is gated by the same private-IP / loopback / metadata-IP filter the rest of the codebase enforces. Surface raised by brodmart in GHSA-h7cc-wwjp-5xqh. Co-authored-by: brodmart * chore: trim comment --------- Co-authored-by: brodmart --- backend/open_webui/routers/images.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index f61970f3de..e55b7c5798 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -442,6 +442,8 @@ GenerateImageForm = CreateImageForm # Alias for backward compatibility async def get_image_data(data: str, headers=None): try: if data.startswith('http://') or data.startswith('https://'): + # Defense-in-depth: gate before fetch (mirrors load_url_image). + validate_url(data) session = await get_session() async with session.get( data, From d3737176bc12952a40ab544d7652ec07a9ad7451 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 17:59:50 +0200 Subject: [PATCH 18/37] fix: require write permission for pin_channel_message on standard channels (#24521) `pin_channel_message` (channels.py:1242) checked `permission='read'` on the standard-channel branch before mutating `is_pinned` / `pinned_by` / `pinned_at` via `Messages.update_is_pinned_by_id`. Pin/unpin is a write operation; gating it on read access let any user with read-only channel access pin or unpin any message in the channel, including admin posts. One-character fix: change `permission='read'` to `permission='write'`. Reported by kikayli in GHSA-5gc6-xhv4-2wg6. Co-authored-by: kikayli --- backend/open_webui/routers/channels.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 487899fccf..7c2ab1ce69 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1256,7 +1256,8 @@ async def pin_channel_message( if not await Channels.is_user_channel_member(channel.id, user.id, db=db): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + # Pin/unpin mutates is_pinned/pinned_by/pinned_at — require write. + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='write', db=db): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) message = await Messages.get_message_by_id(message_id, db=db) From e8e9141061dcdd95b0937673227ccc1a758c5ec5 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 01:02:45 +0900 Subject: [PATCH 19/37] refac --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 88ffd09752..36e29e7069 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH} RUN npm run build ######## WebUI backend ######## -FROM python:3.11.14-slim-bookworm AS base +FROM python:3.11-slim-bookworm AS base # Use args ARG USE_CUDA From f5e110fbee55c320b066c014573342781fcbd634 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 18:03:39 +0200 Subject: [PATCH 20/37] fix: enforce message ownership in group/DM channel update + delete endpoints (#24506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: enforce message ownership in group/DM channel update + delete endpoints `update_message_by_id` (channels.py:1348) and `delete_message_by_id` (channels.py:1550) branch on `channel.type`. The `else` branch (standard channels) correctly enforces `message.user_id != user.id` ownership before mutating, but the `if channel.type in ['group', 'dm']` branch only checked `is_user_channel_member` — channel membership alone, with no message ownership verification. Effect on group/DM channels: any verified member of the conversation could: - overwrite another member's message content while the server preserved `user_id=victim`, producing tampered content that renders to other members as the original author's authentic post (integrity + authenticity); - silently delete another member's messages, removing them from conversation history without trace (integrity). Reproduced end-to-end against v0.9.4 with three users (attacker, victim, viewer) sharing a group channel: attacker overwrites victim's message and deletes another, viewer reads the tampered content as victim-authored. Two patches, identical shape, mirror the `else` branch's existing ownership semantics: - `update_message_by_id` group/DM branch: add `if user.role != 'admin' and message.user_id != user.id: raise 403` immediately after the `is_user_channel_member` check. - `delete_message_by_id` group/DM branch: same. The standard-channel branch is unchanged (it already enforced ownership). Admins remain able to moderate any message, matching the existing semantic in the standard-channel branch. Reports consolidated under GHSA-wwhq-cx22-f7vv (earliest live filing of the group/DM-specific variant). Same gap previously surfaced and partially fixed under GHSA-jxwr-g6r6-j3fx (which addressed the standard-channel branch only) — this completes the cohort. * chore: trim comments --- backend/open_webui/routers/channels.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 7c2ab1ce69..3c7fef8773 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1369,6 +1369,9 @@ async def update_message_by_id( if channel.type in ['group', 'dm']: if not await Channels.is_user_channel_member(channel.id, user.id, db=db): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + # Membership is not authorship — block cross-member edits. + if user.role != 'admin' and message.user_id != user.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: if ( user.role != 'admin' @@ -1570,6 +1573,9 @@ async def delete_message_by_id( if channel.type in ['group', 'dm']: if not await Channels.is_user_channel_member(channel.id, user.id, db=db): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + # Membership is not authorship — block cross-member deletes. + if user.role != 'admin' and message.user_id != user.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: if ( user.role != 'admin' From 841c9045d789005145274955e7ef60b1b11a9be9 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 18:08:12 +0200 Subject: [PATCH 21/37] fix: gate tool content updates behind workspace.tools to match create endpoint (#24513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: gate tool content updates behind workspace.tools to match create endpoint `update_tools_by_id` (routers/tools.py:452) authorizes a caller as long as they are the tool's owner, hold a `write` access grant on the tool, or are an admin. This means a verified user who has been given a write grant on a tool — typically as part of a metadata-collaboration workflow (edit description, adjust valves, manage access grants) — can also overwrite the tool's Python source. Because `load_tool_module_by_id` further down calls `exec(content, module.__dict__)` at module-import time, anything the new content puts outside the `class Tools:` body executes immediately on the server with the worker's privileges (root in the default Docker deployment). The `create_new_tools` endpoint already requires `workspace.tools` (or `workspace.tools_import`) precisely because creating a tool means submitting executable code. The update endpoint did not mirror that check, producing an asymmetric authorization surface in which a write-grantee with no workspace permission can still reach the same exec sink as a workspace.tools-trusted creator. SECURITY.md frames `workspace.tools` as the trust signal an admin uses to delegate code-execution capability; the previous behavior let that signal be bypassed by a per-resource share. Fix: after the existing ownership / write-grant / admin gate, add a content-change check. If `form_data.content != tools.content`, require `workspace.tools` or `workspace.tools_import` (or admin role). Metadata edits — `name`, `description`, valves config, access grants — continue to flow through the existing gate, so the legitimate share-for- collaboration workflow is unaffected. Reported by KadirArslan in GHSA-p4fx-23fq-jfg6 with a working three-user PoC (Alice trusted with workspace.tools creates a tool and shares write to Bob; Bob updates content and the new code runs as root inside the container, with Burp Collaborator confirming outbound exfiltration). Co-authored-by: KadirArslan * chore: trim comment --------- Co-authored-by: KadirArslan --- backend/open_webui/routers/tools.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 04d845c3de..3eddefdee6 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -480,6 +480,19 @@ async def update_tools_by_id( detail=ERROR_MESSAGES.UNAUTHORIZED, ) + # Content edits trigger exec on load — gate them behind workspace.tools (matches /create). + if form_data.content != tools.content: + if user.role != 'admin' and not ( + await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db) + or await has_permission( + user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + try: form_data.content = replace_imports(form_data.content) tool_module, frontmatter = await load_tool_module_by_id(id, content=form_data.content) From d11e06f1b7f6f6298c31432d88fec7c4a40c7499 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 18:09:15 +0200 Subject: [PATCH 22/37] fix: prevent redirect-based SSRF and enforce collecton write access (#24524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: prevent redirect-based SSRF in get_image_base64_from_url Cohort follow-up to PR #24491. That PR patched three call sites (SafeWebBaseLoader._scrape, get_content_from_url, load_url_image) to pass allow_redirects=False on the underlying HTTP client; this fourth call site in utils/files.py was missed. get_image_base64_from_url() is invoked from convert_url_images_to_base64 in utils/middleware.py on every /api/chat/completions request whose message content includes an image_url part. validate_url() is called on the originally-submitted URL only; the aiohttp session.get() call had no allow_redirects argument and the shared session pool does not override the aiohttp default (allow_redirects=True). An authenticated user sending a chat message with image_url pointing at an attacker host that 302-redirects to 169.254.169.254 / 127.0.0.1 / RFC1918 reached the internal target. This is the most reachable variant in the redirect cluster: no special endpoint, no admin permission, no feature flag. Apply the same one-line fix as the other three call sites: pass allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS (defaults to False). Reported by nayakchinmohan in GHSA-88jq-grjp-jx6f; consolidated under GHSA-rh5x-h6pp-cjj6. Co-authored-by: nayakchinmohan * fix: enforce collection write access on process_file endpoint Cohort follow-up to ba83613ff. That commit added _validate_collection_access to process_text and process_web (the user-supplied collection_name path) but missed process_file in the same router. process_file accepts a user-supplied collection_name and writes the file's embedded content into that collection via save_docs_to_vector_db. The file_id is gated by file ownership (line 1562) but collection_name was unchecked, so an authenticated user could append content from a file they own into another user's knowledge-base collection by passing the victim's KB UUID as collection_name. Identical pattern to the process_text and process_web gaps that ba83613ff closed. Apply the same one-line gate as the sibling endpoints: when collection_name is user-supplied (not the default file-{file.id} fallback), require write access via _validate_collection_access. The shared validator delegates to filter_accessible_collections, which already correctly handles file-* prefixes (via has_access_to_file) and KB UUIDs (via Knowledges.check_access_by_user_id) — admins bypass. Reported by tenbbughunters (Tenable) in GHSA-4g37-7p2c-38r9 (the comprehensive write-path filing covering process_text / process_file / process_web / process_youtube and the _validate_collection_access UUID root cause), and independently re-identified for the missed process_file call site by kodareef5 in GHSA-4m74-3cmc-293g. Co-authored-by: tenbbughunters Co-authored-by: kodareef5 * fix: enforce collection write access on process_files_batch endpoint Cohort follow-up to ba83613ff and the prior process_file fix on this branch. process_files_batch (line 2604) is the third write endpoint in the same router that accepts a user-supplied collection_name; it was covered in the same Tenable filing as process_file and was missed by the same cohort fix. The endpoint validates per-file ownership at line 2642 but does not check whether the caller has write access to the target collection_name before save_docs_to_vector_db writes into it at line 2683-2690 with add=True. Apply the same one-line gate as the sibling endpoints. Validate only when collection_name is user-supplied (truthy) so the existing fall through behavior for the None case is unchanged. Same Tenable / kodareef5 cohort as the previous commit. Co-authored-by: tenbbughunters Co-authored-by: kodareef5 --------- Co-authored-by: nayakchinmohan Co-authored-by: tenbbughunters Co-authored-by: kodareef5 --- backend/open_webui/routers/retrieval.py | 5 +++++ backend/open_webui/utils/files.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index dfd503f035..201e6a63fb 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1567,6 +1567,8 @@ async def process_file( if collection_name is None: collection_name = f'file-{file.id}' + else: + await _validate_collection_access([collection_name], user, access_type='write') if form_data.content: # Update the content in the file @@ -2617,6 +2619,9 @@ async def process_files_batch( collection_name = form_data.collection_name + if collection_name: + await _validate_collection_access([collection_name], user, access_type='write') + file_results: List[BatchProcessFilesResult] = [] file_errors: List[BatchProcessFilesResult] = [] file_updates: List[FileUpdateForm] = [] diff --git a/backend/open_webui/utils/files.py b/backend/open_webui/utils/files.py index 8149987fe4..6b821d58b6 100644 --- a/backend/open_webui/utils/files.py +++ b/backend/open_webui/utils/files.py @@ -26,7 +26,11 @@ import base64 import io import re -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK +from open_webui.env import ( + AIOHTTP_CLIENT_ALLOW_REDIRECTS, + AIOHTTP_CLIENT_SESSION_SSL, + ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK, +) from open_webui.utils.session_pool import get_session BASE64_IMAGE_URL_PREFIX = re.compile(r'data:image/\w+;base64,', re.IGNORECASE) @@ -53,11 +57,17 @@ _IMAGE_MIME_FALLBACK = { async def get_image_base64_from_url(url: str) -> Optional[str]: try: if url.startswith('http'): - # Validate URL to prevent SSRF attacks against local/private networks + # Validate URL to prevent SSRF attacks against local/private networks. + # allow_redirects=False prevents redirect-based SSRF: validate_url() is + # called only on the originally-submitted URL; following 3xx redirects + # without re-validation would let an attacker reach private IPs via a + # public host that redirects internally (e.g. cloud-metadata exfil). validate_url(url) # Download the image from the URL session = await get_session() - async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as response: + async with session.get( + url, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS + ) as response: response.raise_for_status() image_data = await response.read() encoded_string = base64.b64encode(image_data).decode('utf-8') From 8d3133fe2835122bffaa4f2ce584730bc9c78981 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 01:15:34 +0900 Subject: [PATCH 23/37] refac --- backend/open_webui/config.py | 5 +++++ backend/open_webui/routers/terminals.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 4cc39e11c4..dc61ad5cd7 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1226,6 +1226,11 @@ TERMINAL_SERVER_CONNECTIONS = PersistentConfig( terminal_server_connections, ) +try: + TERMINAL_PROXY_HEADERS = json.loads(os.environ.get('TERMINAL_PROXY_HEADERS', '{}')) +except Exception: + TERMINAL_PROXY_HEADERS = {} + #################################### # WEBUI #################################### diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index 003db06968..c251b20d48 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -17,6 +17,7 @@ from starlette.background import BackgroundTask from open_webui.utils.auth import get_verified_user from open_webui.utils.access_control import has_connection_access from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL +from open_webui.config import TERMINAL_PROXY_HEADERS from open_webui.models.groups import Groups from open_webui.models.users import Users @@ -151,6 +152,8 @@ async def proxy_terminal( for key, value in upstream_response.headers.items() if key.lower() not in STRIPPED_RESPONSE_HEADERS } + if TERMINAL_PROXY_HEADERS: + filtered_headers.update(TERMINAL_PROXY_HEADERS) # Stream binary responses directly if any(t in upstream_content_type for t in STREAMING_CONTENT_TYPES): From fc94118b2d20a051ef04e2c2db94c426f54f2d6a Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 18:16:17 +0200 Subject: [PATCH 24/37] fix: prevent mass-assignment user_id spoofing in POST /api/v1/evaluations/feedback (#24508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: prevent mass-assignment user_id spoofing in POST /api/v1/evaluations/feedback Two independent gaps in backend/open_webui/models/feedbacks.py let an authenticated caller forge the `user_id` (and `id`, `version`) on a new feedback record submitted to POST /api/v1/evaluations/feedback: 1. `FeedbackForm` declared `model_config = ConfigDict(extra='allow')`, so Pydantic preserved any extra fields supplied in the request body — including `user_id`, `id`, `version`. The form is the public input boundary for the endpoint and should not accept unknown fields. 2. In `insert_new_feedback`, the dict literal placed `**form_data.model_dump()` AFTER `'id': id`, `'user_id': user_id`, `'version': 0`. Python dict-literal duplicate-key resolution is last-wins, so any of those fields present in `form_data` overwrote the server-derived values. Combined effect: a regular user could POST a feedback record with an arbitrary `user_id`, attributing the rating to any other user. The Elo leaderboard at backend/open_webui/routers/evaluations.py computes model rankings from these records, and the admin export (GET /api/v1/evaluations/feedbacks/export) and admin list (GET /api/v1/evaluations/feedbacks/all) display the spoofed attribution. Two fixes, defense-in-depth: - FeedbackForm: switch `extra='allow'` to `extra='ignore'` so Pydantic drops unknown fields at parse time. Sub-models (RatingData / MetaData / SnapshotData) intentionally keep `extra='allow'` because their contents are deliberately schema-flexible — the spoofing surface was the form, not the sub-payloads. - insert_new_feedback: spread `form_data.model_dump()` first, then overlay server-controlled fields (`id`, `user_id`, `version`, `created_at`, `updated_at`) so the explicit keys win on duplicate-key resolution regardless of what reaches the function. Matches the secure pattern already used in backend/open_webui/models/functions.py:120. Reported by yantongggg in GHSA-rjmp-vjf2-qf4g. Same root-cause class as the prior published GHSA-hr43-rjmr-7wmm (folder mass-assignment, fixed in v0.9.0); that fix did not generalize across the codebase, this fix closes the feedback variant. Co-authored-by: yantongggg * chore: trim comments --------- Co-authored-by: yantongggg --- backend/open_webui/models/feedbacks.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index 02f61f82ee..d8ae4dc9b1 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -103,7 +103,8 @@ class FeedbackForm(BaseModel): data: Optional[RatingData] = None meta: Optional[dict] = None snapshot: Optional[SnapshotData] = None - model_config = ConfigDict(extra='allow') + # ignore: drop client-supplied id/user_id/version/timestamps at parse time. + model_config = ConfigDict(extra='ignore') class UserResponse(BaseModel): @@ -145,12 +146,13 @@ class FeedbackTable: ) -> Optional[FeedbackModel]: async with get_async_db_context(db) as db: id = str(uuid.uuid4()) + # Spread form_data first so server-controlled fields win on duplicate keys. feedback = FeedbackModel( **{ + **form_data.model_dump(), 'id': id, 'user_id': user_id, 'version': 0, - **form_data.model_dump(), 'created_at': int(time.time()), 'updated_at': int(time.time()), } From d1ef5382377f590f97a6dbaee88f369e6d7c5f6f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 01:31:46 +0900 Subject: [PATCH 25/37] refac --- .../Models/DefaultFiltersSelector.svelte | 33 ++++++-------- .../workspace/Models/FiltersSelector.svelte | 43 +++++++------------ 2 files changed, 29 insertions(+), 47 deletions(-) diff --git a/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte b/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte index d03928a56c..8828a17ffa 100644 --- a/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte +++ b/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte @@ -1,5 +1,5 @@
@@ -30,21 +17,27 @@
{#if filters.length > 0}
- {#each Object.keys(_filters) as filter, filterIdx} + {#each filters as filter} + {@const isSelected = selectedFilterIds.includes(filter.id)}
{ - _filters[filter].selected = e.detail === 'checked'; - selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected); + if (e.detail === 'checked') { + if (!selectedFilterIds.includes(filter.id)) { + selectedFilterIds = [...selectedFilterIds, filter.id]; + } + } else { + selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id); + } }} />
- - {_filters[filter].name} + + {filter.name}
diff --git a/src/lib/components/workspace/Models/FiltersSelector.svelte b/src/lib/components/workspace/Models/FiltersSelector.svelte index c5207e61b7..7432107daf 100644 --- a/src/lib/components/workspace/Models/FiltersSelector.svelte +++ b/src/lib/components/workspace/Models/FiltersSelector.svelte @@ -1,5 +1,5 @@ {#if filters.length > 0} @@ -28,31 +15,33 @@
{$i18n.t('Filters')}
- +
- {#each Object.keys(_filters) as filter, filterIdx} + {#each filters as filter} + {@const isSelected = filter.is_global || selectedFilterIds.includes(filter.id)}
{ - if (!_filters[filter].is_global) { - _filters[filter].selected = e.detail === 'checked'; - selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected); + if (filter.is_global) return; + + if (e.detail === 'checked') { + if (!selectedFilterIds.includes(filter.id)) { + selectedFilterIds = [...selectedFilterIds, filter.id]; + } + } else { + selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id); } }} />
- - {_filters[filter].name} + + {filter.name}
From 1388f4568b8f508c26542673dd01f1fa049e798a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 01:46:33 +0900 Subject: [PATCH 26/37] refac --- backend/open_webui/models/chats.py | 75 ++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index af999f21cd..51f2f121b3 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -459,24 +459,89 @@ class ChatTable: return None return row[0] or 'New Chat' + @staticmethod + def get_unresolved_parent_ids(messages_map: dict) -> set[str]: + """Return parent IDs referenced by messages but absent from the map. + + An empty set means the message graph is fully connected. + """ + return { + msg['parentId'] + for msg in messages_map.values() + if msg.get('parentId') and msg['parentId'] not in messages_map + } + + async def backfill_messages_by_chat_id( + self, chat_id: str, user_id: str, messages: dict[str, dict] + ) -> None: + """Write messages to the ``chat_message`` table so future lookups + use the fast path. Errors are logged but never raised. + """ + for message_id, message in messages.items(): + if not isinstance(message, dict) or not message.get('role'): + continue + try: + await ChatMessages.upsert_message( + message_id=message_id, + chat_id=chat_id, + user_id=user_id, + data=message, + ) + except Exception as e: + log.warning('Backfill failed for message %s in chat %s: %s', message_id, chat_id, e) + async def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]: """Message map for walking history (see ``get_message_list``). - Prefer ``chat_message`` rows to avoid loading the large ``chat`` - JSON blob; fall back to embedded history when no rows exist - (legacy chats). + Prefer ``chat_message`` rows to avoid loading the large embedded + history; fall back to the legacy JSON when no rows exist. + When rows exist but the parent-link graph has gaps (e.g. migration + failures), missing messages are merged from the legacy history + and backfilled so future requests self-heal. """ # Fast path: build from normalized chat_message rows. messages_map = await ChatMessages.get_messages_map_by_chat_id(id) + if messages_map is not None: + unresolved_ids = self.get_unresolved_parent_ids(messages_map) + if not unresolved_ids: + return messages_map + + # Graph has gaps — enrich from the legacy embedded history. + log.info( + 'Chat %s: %d unresolved parent reference(s) in chat_message — ' + 'enriching from legacy history', + id, len(unresolved_ids), + ) + chat = await self.get_chat_by_id(id) + if chat: + history_messages = chat.chat.get('history', {}).get('messages', {}) or {} + missing_messages = { + message_id: history_messages[message_id] + for message_id in unresolved_ids + if message_id in history_messages + } + + if missing_messages: + messages_map.update(missing_messages) + + # Backfill so future requests use the fast path. + await self.backfill_messages_by_chat_id(id, chat.user_id, missing_messages) + return messages_map - # No rows — fall back to the embedded JSON blob for legacy chats. + # No rows — fall back to the legacy embedded history. chat = await self.get_chat_by_id(id) if chat is None: return None - return chat.chat.get('history', {}).get('messages', {}) or {} + history_messages = chat.chat.get('history', {}).get('messages', {}) or {} + + # Backfill so future requests use the fast path. + if history_messages: + await self.backfill_messages_by_chat_id(id, chat.user_id, history_messages) + + return history_messages async def get_message_by_id_and_message_id(self, id: str, message_id: str) -> Optional[dict]: chat = await self.get_chat_by_id(id) From 3bba1c227059a44c7eeefa97b8c69a63bf4f3454 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 01:56:02 +0900 Subject: [PATCH 27/37] feat: add IFRAME_CSP env var for srcdoc iframe content security policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an IFRAME_CSP environment variable that injects a Content-Security-Policy tag into all srcdoc iframes rendering untrusted content: - Artifacts (LLM-generated HTML previews) - FullHeightIframe (tool/embed output) - FilePreview (user-uploaded HTML files) - CitationModal (RAG document HTML) Shared utility in src/lib/utils/csp.ts handles injection with HTML-safe attribute escaping. URL-based iframes (src=) are correctly excluded. Env-var only — no PersistentConfig, no admin UI, no DB. Set once at deploy time, requires restart. Empty string (default) means no CSP restriction. --- backend/open_webui/config.py | 1 + backend/open_webui/main.py | 2 ++ src/lib/components/chat/Artifacts.svelte | 4 +++- src/lib/components/chat/FileNav/FilePreview.svelte | 5 +++-- .../chat/Messages/Citations/CitationModal.svelte | 5 +++-- src/lib/components/common/FullHeightIframe.svelte | 4 +++- src/lib/stores/index.ts | 1 + src/lib/utils/csp.ts | 14 ++++++++++++++ 8 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 src/lib/utils/csp.ts diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index dc61ad5cd7..6a29504cb6 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1376,6 +1376,7 @@ RESPONSE_WATERMARK = PersistentConfig( os.environ.get('RESPONSE_WATERMARK', ''), ) +IFRAME_CSP = os.environ.get('IFRAME_CSP', '') USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = ( os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true' diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 52e59d26e0..4adece3825 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -460,6 +460,7 @@ from open_webui.config import ( OAUTH_PROVIDERS, WEBUI_URL, RESPONSE_WATERMARK, + IFRAME_CSP, # Admin ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_ANALYTICS, @@ -2444,6 +2445,7 @@ async def get_app_config(request: Request): 'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE, 'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT, 'response_watermark': app.state.config.RESPONSE_WATERMARK, + 'iframe_csp': IFRAME_CSP, }, 'license_metadata': app.state.LICENSE_METADATA, **( diff --git a/src/lib/components/chat/Artifacts.svelte b/src/lib/components/chat/Artifacts.svelte index 7e0bdf2060..f8483d122e 100644 --- a/src/lib/components/chat/Artifacts.svelte +++ b/src/lib/components/chat/Artifacts.svelte @@ -7,12 +7,14 @@ import { artifactCode, chatId, + config, settings, showArtifacts, showControls, artifactContents } from '$lib/stores'; import { copyToClipboard, createMessagesList } from '$lib/utils'; + import { injectCsp } from '$lib/utils/csp'; import XMark from '../icons/XMark.svelte'; import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte'; @@ -242,7 +244,7 @@ {:else} diff --git a/src/lib/components/common/FullHeightIframe.svelte b/src/lib/components/common/FullHeightIframe.svelte index fe629c8b59..1bb18943a1 100644 --- a/src/lib/components/common/FullHeightIframe.svelte +++ b/src/lib/components/common/FullHeightIframe.svelte @@ -1,5 +1,7 @@
- { - const { lang, text: code } = token; + {#if $settings?.renderMarkdownInAssistantMessages ?? true} + { + const { lang, text: code } = token; - if ( - ($settings?.detectArtifacts ?? true) && - (['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) && - !$mobile && - $chatId - ) { - await tick(); - showArtifacts.set(true); - showControls.set(true); - } - }} - onPreview={async (value) => { - console.log('Preview', value); - await artifactCode.set(value); - await showControls.set(true); - await showArtifacts.set(true); - await showEmbeds.set(false); - }} - /> + if ( + ($settings?.detectArtifacts ?? true) && + (['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) && + !$mobile && + $chatId + ) { + await tick(); + showArtifacts.set(true); + showControls.set(true); + } + }} + onPreview={async (value) => { + console.log('Preview', value); + await artifactCode.set(value); + await showControls.set(true); + await showArtifacts.set(true); + await showEmbeds.set(false); + }} + /> + {:else} + {@const extracted = extractDetailsBlocks(content)} + + {#if extracted.detailsContent} + + + {/if} + {#if extracted.plainContent} +
{extracted.plainContent}
+ {/if} + {/if}
{#if floatingButtons} diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte index acc73c6446..f4f5a94c74 100644 --- a/src/lib/components/chat/Messages/UserMessage.svelte +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -376,12 +376,16 @@ : ' w-full'}" > {#if message.content} - + {#if $settings?.renderMarkdownInUserMessages ?? true} + + {:else} +
{message.content}
+ {/if} {/if}
diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index ee242fb735..250c75a9ff 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -69,6 +69,8 @@ let temporaryChatByDefault = false; let chatFadeStreamingText = true; let collapseCodeBlocks = false; + let renderMarkdownInUserMessages = true; + let renderMarkdownInAssistantMessages = true; let expandDetails = false; let renderMarkdownInPreviews = true; let showChatTitleInTab = true; @@ -232,6 +234,8 @@ copyFormatted = $settings?.copyFormatted ?? false; collapseCodeBlocks = $settings?.collapseCodeBlocks ?? false; + renderMarkdownInUserMessages = $settings?.renderMarkdownInUserMessages ?? true; + renderMarkdownInAssistantMessages = $settings?.renderMarkdownInAssistantMessages ?? true; expandDetails = $settings?.expandDetails ?? false; renderMarkdownInPreviews = $settings?.renderMarkdownInPreviews ?? true; @@ -776,6 +780,44 @@
+
+
+
+ {$i18n.t('Render Markdown in User Messages')} +
+ +
+ { + saveSettings({ renderMarkdownInUserMessages }); + }} + /> +
+
+
+ +
+
+
+ {$i18n.t('Render Markdown in Assistant Messages')} +
+ +
+ { + saveSettings({ renderMarkdownInAssistantMessages }); + }} + /> +
+
+
+
diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index e3a291a956..49e1c6f56e 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -1697,6 +1697,8 @@ "Remove Model": "", "Rename": "", "Renamed to {{name}}": "", + "Render Markdown in Assistant Messages": "", + "Render Markdown in User Messages": "", "Render Markdown in Previews": "", "Reorder Models": "", "Repeats": "", From 2dbf7b6764a7922458d3b0139687ad6dcd7596d9 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:12:38 +0900 Subject: [PATCH 30/37] refac --- backend/open_webui/routers/folders.py | 31 ++++++++-------- backend/open_webui/routers/knowledge.py | 18 ++++++++++ .../open_webui/utils/access_control/files.py | 35 +++++++++++++++++++ backend/open_webui/utils/middleware.py | 9 +++-- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index ebd0c0cb17..7dda918821 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -16,8 +16,6 @@ from open_webui.models.folders import ( Folders, ) from open_webui.models.chats import Chats -from open_webui.models.files import Files -from open_webui.models.knowledge import Knowledges from open_webui.config import UPLOAD_DIR @@ -32,6 +30,7 @@ from fastapi.responses import FileResponse, StreamingResponse from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control.files import get_accessible_folder_files log = logging.getLogger(__name__) @@ -75,20 +74,10 @@ async def get_folders( if folder.parent_id and not await Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db): folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db) - if folder.data: - if 'files' in folder.data: - valid_files = [] - for file in folder.data['files']: - if file.get('type') == 'file': - if await Files.check_access_by_user_id(file.get('id'), user.id, 'read', db=db): - valid_files.append(file) - elif file.get('type') == 'collection': - if await Knowledges.check_access_by_user_id(file.get('id'), user.id, 'read', db=db): - valid_files.append(file) - else: - valid_files.append(file) - - folder.data['files'] = valid_files + if folder.data and 'files' in folder.data: + accessible_files = await get_accessible_folder_files(folder.data['files'], user, db=db) + if len(accessible_files) != len(folder.data.get('files', [])): + folder.data['files'] = accessible_files await Folders.update_folder_by_id_and_user_id( folder.id, user.id, FolderUpdateForm(data=folder.data), db=db ) @@ -173,6 +162,16 @@ async def update_folder_name_by_id( detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), ) + # Validate read access to every file/collection being attached. + # Folder files are consumed by chat middleware as RAG context. + if form_data.data and isinstance(form_data.data.get('files'), list): + accessible_files = await get_accessible_folder_files(form_data.data['files'], user, db=db) + if len(accessible_files) != len(form_data.data['files']): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + try: folder = await Folders.update_folder_by_id_and_user_id(id, user.id, form_data, db=db) return folder diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index f503169fc0..8ff987b610 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -31,6 +31,7 @@ from open_webui.storage.provider import Storage from open_webui.constants import ERROR_MESSAGES from open_webui.utils.auth import get_verified_user, get_admin_user from open_webui.utils.access_control import has_permission, filter_allowed_access_grants +from open_webui.utils.access_control.files import has_access_to_file from open_webui.models.access_grants import AccessGrants @@ -656,6 +657,14 @@ async def add_file_to_knowledge_by_id( detail=ERROR_MESSAGES.FILE_NOT_PROCESSED, ) + # KB write-access alone is not enough — caller must also be able to read the file. + if file.user_id != user.id and user.role != 'admin': + if not await has_access_to_file(file.id, 'read', user, db=db): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + # Add content to the vector database try: await process_file( @@ -1017,6 +1026,15 @@ async def add_files_to_knowledge_batch( detail=f'File {missing_ids[0]} not found', ) + # Per-file read-access check — same gate as the single-file endpoint. + if user.role != 'admin': + for file in files: + if file.user_id != user.id and not await has_access_to_file(file.id, 'read', user, db=db): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + # Process files try: result = await process_files_batch( diff --git a/backend/open_webui/utils/access_control/files.py b/backend/open_webui/utils/access_control/files.py index a48dfeb0f1..fb318e3c66 100644 --- a/backend/open_webui/utils/access_control/files.py +++ b/backend/open_webui/utils/access_control/files.py @@ -87,3 +87,38 @@ async def has_access_to_file( return True return False + + +async def get_accessible_folder_files( + entries: list[dict] | None, + user: UserModel, + db: AsyncSession | None = None, +) -> list[dict]: + """Filter folder.data['files'] entries to those the caller can read. + + Each entry is expected to have 'type' ('file' or 'collection') and 'id'. + Admins bypass all checks. Unknown types are kept as-is. + """ + if not entries: + return [] + if user.role == 'admin': + return list(entries) + + accessible: list[dict] = [] + for entry in entries: + if not isinstance(entry, dict): + continue + entry_type = entry.get('type') + entry_id = entry.get('id') + if not entry_id: + accessible.append(entry) + continue + if entry_type == 'file': + if await has_access_to_file(entry_id, 'read', user, db=db): + accessible.append(entry) + elif entry_type == 'collection': + if await Knowledges.check_access_by_user_id(entry_id, user.id, 'read', db=db): + accessible.append(entry) + else: + accessible.append(entry) + return accessible diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 3e25effa9a..60796fa22a 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -106,6 +106,7 @@ from open_webui.utils.tools import ( get_terminal_tools, ) from open_webui.utils.access_control import has_connection_access +from open_webui.utils.access_control.files import get_accessible_folder_files from open_webui.utils.plugin import load_function_module_by_id from open_webui.utils.filter import ( get_sorted_filter_ids, @@ -2407,15 +2408,17 @@ async def process_chat_payload(request, form_data, user, metadata, model): if 'system_prompt' in folder.data: form_data = await apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user) if 'files' in folder.data: + # Defensive: filter to entries the caller can still read. + allowed_files = await get_accessible_folder_files(folder.data['files'], user) if metadata.get('params', {}).get('function_calling') != 'native': form_data['files'] = [ - *folder.data['files'], + *allowed_files, *form_data.get('files', []), ] else: # Native FC: skip RAG injection, builtin tools # will read folder knowledge from metadata. - metadata['folder_knowledge'] = folder.data['files'] + metadata['folder_knowledge'] = allowed_files # Model "Knowledge" handling user_message = get_last_user_message(form_data['messages']) @@ -2615,7 +2618,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id) if folder and folder.data and 'files' in folder.data: files = [f for f in files if f.get('id', None) != folder_id] - files = [*files, *folder.data['files']] + files = [*files, *await get_accessible_folder_files(folder.data['files'], user)] # files = [*files, *[{"type": "url", "url": url, "name": url} for url in urls]] # Remove duplicate files based on their content From 3a21b334cce30226750c5c537345dc51bb8bef17 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:15:46 +0900 Subject: [PATCH 31/37] refac --- backend/open_webui/routers/chats.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 8bfe5dfc51..9c4609477c 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -1412,19 +1412,16 @@ async def update_shared_chat_access_by_id( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) + else: + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if not chat: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if chat.user_id != user.id and user.role != 'admin': - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - form_data.access_grants = await filter_allowed_access_grants( request.app.state.config.USER_PERMISSIONS, user.id, @@ -1449,19 +1446,16 @@ async def get_shared_chat_access_by_id( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) + else: + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if not chat: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if chat.user_id != user.id and user.role != 'admin': - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - grants = await AccessGrants.get_grants_by_resource('shared_chat', id, db=db) return [ { From 15e696691cad98692c329de62ed8a5bdb3a26d4e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:25:11 +0900 Subject: [PATCH 32/37] refac --- backend/open_webui/env.py | 9 +++++++++ backend/open_webui/routers/users.py | 12 +++++++++--- backend/open_webui/utils/validate.py | 14 +++++--------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 1ab18fe1c7..903b3effcc 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -260,6 +260,15 @@ ENABLE_EASTER_EGGS = os.environ.get('ENABLE_EASTER_EGGS', 'True').lower() == 'tr # controlled origins) and fall through to the default image instead. ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.environ.get('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true' +PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset( + t.strip() + for t in os.environ.get( + 'PROFILE_IMAGE_ALLOWED_MIME_TYPES', + 'image/png,image/jpeg,image/gif,image/webp', + ).split(',') + if t.strip() +) + #################################### # WEBUI_BUILD_HASH #################################### diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 7fe5fcd2dc..33d1cd425c 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -29,7 +29,7 @@ from open_webui.models.users import ( ) from open_webui.constants import ERROR_MESSAGES -from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, STATIC_DIR +from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES, STATIC_DIR from open_webui.internal.db import get_async_session @@ -494,12 +494,18 @@ async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_u header, base64_data = user.profile_image_url.split(',', 1) image_data = base64.b64decode(base64_data) image_buffer = io.BytesIO(image_data) - media_type = header.split(';')[0].lstrip('data:') + media_type = header.split(';')[0].lstrip('data:').lower() + + if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES: + return FileResponse(f'{STATIC_DIR}/user.png') return StreamingResponse( image_buffer, media_type=media_type, - headers={'Content-Disposition': 'inline'}, + headers={ + 'Content-Disposition': 'inline', + 'X-Content-Type-Options': 'nosniff', + }, ) except Exception as e: pass diff --git a/backend/open_webui/utils/validate.py b/backend/open_webui/utils/validate.py index 1e98b41105..68a56dfadc 100644 --- a/backend/open_webui/utils/validate.py +++ b/backend/open_webui/utils/validate.py @@ -3,17 +3,13 @@ import re from urllib.parse import urlparse -# Matches the OWUI-generated profile image route. ``[^/?#]+`` accepts -# any user-ID without allowing path-traversal or query/fragment injection, -# and the ``$`` anchor rejects trailing path components. +from open_webui.env import PROFILE_IMAGE_ALLOWED_MIME_TYPES + _USER_PROFILE_IMAGE_RE = re.compile(r'^/api/v1/users/[^/?#]+/profile/image$') -# Validates MIME type and structure of base64 data URIs. Only the prefix -# is checked — validating the full base64 payload would mean running a -# regex across megabytes of data on every Pydantic instantiation for zero -# security benefit (corrupt base64 simply renders a broken image, same as -# a 404 URL). SVG is intentionally excluded: it can carry embedded scripts. -_SAFE_DATA_URI_RE = re.compile(r'^data:image/(png|jpeg|gif|webp);base64,', re.IGNORECASE) +# Data-URI prefix validator derived from PROFILE_IMAGE_ALLOWED_MIME_TYPES. +_mime_suffixes = '|'.join(re.escape(t.split('/')[-1]) for t in sorted(PROFILE_IMAGE_ALLOWED_MIME_TYPES)) +_SAFE_DATA_URI_RE = re.compile(rf'^data:image/({_mime_suffixes});base64,', re.IGNORECASE) # Exact relative paths accepted as profile images. These are the only # static-asset paths OWUI itself assigns; no prefix/wildcard matching is From 39777e35d87dc88e446c8859f552ad4e2d24d5ca Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:28:38 +0900 Subject: [PATCH 33/37] doc: changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1930256b..1ea4efac17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 🛡️ **Redirect-based SSRF protection.** All outbound HTTP requests now block 3xx redirects by default via a new `AIOHTTP_CLIENT_ALLOW_REDIRECTS` environment variable, preventing redirect-based SSRF where a public URL silently redirects to internal addresses (RFC 1918, loopback, cloud-metadata endpoints). Affected call sites include web fetch, image loading, OAuth discovery, tool server execution, and code interpreter login. [#24491](https://github.com/open-webui/open-webui/pull/24491) +- 🛡️ **Iframe content security policy.** Administrators can now configure a Content-Security-Policy for all srcdoc iframes (Artifacts, tool embeds, file previews, citation modals) via the `IFRAME_CSP` environment variable, restricting what LLM-generated or user-uploaded HTML can load and execute inside previews. [Commit](https://github.com/open-webui/open-webui/commit/3bba1c227059a44c7eeefa97b8c69a63bf4f3454) +- 🎛️ **Granular markdown rendering controls.** Users can now independently disable Markdown rendering for user messages and assistant responses from Interface settings, preventing unintended formatting when pasting text that contains Markdown-sensitive characters. [Commit](https://github.com/open-webui/open-webui/commit/4a1064cefd6f48a8b3b02cd31f77838c8802b635) +- 🔧 **Terminal proxy response headers.** Administrators can now inject custom response headers into terminal proxy responses via the `TERMINAL_PROXY_HEADERS` environment variable (JSON object), enabling deployment-specific security headers like sandbox policies for proxied content. [Commit](https://github.com/open-webui/open-webui/commit/8d3133fe2835122bffaa4f2ce584730bc9c78981) ### Fixed - 📝 **Notes create and open reliability.** Creating new notes and opening existing notes no longer fails with a TypeError caused by `is_pinned` being passed to the SQLAlchemy model on create, and passed twice to `NoteResponse` on read. [#24484](https://github.com/open-webui/open-webui/issues/24484), [#24486](https://github.com/open-webui/open-webui/pull/24486) - 🔐 **Skill public sharing permission enforcement.** Creating or updating skills now filters access grants through the `sharing.public_skills` permission, preventing non-admin users from making skills publicly accessible without the required permission. [#24494](https://github.com/open-webui/open-webui/pull/24494) - 🔐 **Calendar public sharing permission enforcement.** Creating or updating calendars now filters access grants through a new `sharing.public_calendars` permission, preventing users from making calendars publicly readable or writable without explicit admin-granted sharing permission. [#24493](https://github.com/open-webui/open-webui/pull/24493) +- 🔐 **Feedback user attribution spoofing.** Submitting evaluation feedback can no longer forge the `user_id` field through mass-assignment, preventing authenticated users from attributing ratings to other users and corrupting Elo leaderboard rankings and admin feedback exports. [#24508](https://github.com/open-webui/open-webui/pull/24508) +- 🛡️ **Image URL redirect-based SSRF.** Chat messages containing image URLs no longer follow 3xx redirects to internal addresses during base64 conversion, closing the most reachable redirect-based SSRF variant that required no special permissions or feature flags. [#24524](https://github.com/open-webui/open-webui/pull/24524) +- 🛡️ **Collection write access on file processing.** The `process_file` and `process_files_batch` retrieval endpoints now enforce collection write-access checks before embedding content, preventing authenticated users from injecting file content into another user's knowledge-base collection. [#24524](https://github.com/open-webui/open-webui/pull/24524) +- 🔐 **Tool source code update authorization.** Updating a tool's Python source code now requires `workspace.tools` or `workspace.tools_import` permission, preventing users with only a write-access grant from overwriting executable tool code while still allowing metadata edits. [#24513](https://github.com/open-webui/open-webui/pull/24513) +- 🔐 **Channel message ownership enforcement.** Updating or deleting messages in group and DM channels now requires message ownership, preventing channel members from tampering with or silently removing other members' messages. [#24506](https://github.com/open-webui/open-webui/pull/24506) +- 🔐 **Channel pin write permission.** Pinning and unpinning messages on standard channels now requires write permission instead of read permission, preventing read-only users from modifying pinned content. [#24521](https://github.com/open-webui/open-webui/pull/24521) +- 🛡️ **Image generation URL validation.** Generated image URLs are now validated through `validate_url()` before fetching, aligning the defense-in-depth posture with sibling image-loading paths. [#24518](https://github.com/open-webui/open-webui/pull/24518) +- 🔐 **Model params exposure for read-only users.** The per-model API endpoint now strips the `params` dict (including system prompts) from responses to callers without write access, preventing read-only users from viewing admin-curated model configuration. [#24525](https://github.com/open-webui/open-webui/pull/24525) +- 🛡️ **URL parser SSRF bypass.** URL validation now rejects backslash, tab, CR, and LF characters that cause urllib and requests/aiohttp to disagree on the target host, closing a parser-confusion SSRF bypass. [#24534](https://github.com/open-webui/open-webui/pull/24534) +- 🛡️ **Profile image MIME-type allowlist.** Serving profile images from data URIs now enforces a strict MIME-type allowlist (PNG, JPEG, GIF, WEBP by default, configurable via `PROFILE_IMAGE_ALLOWED_MIME_TYPES`) and sets `X-Content-Type-Options: nosniff`, preventing stored-XSS through SVG or other executable content types. [Commit](https://github.com/open-webui/open-webui/commit/15e696691cad98692c329de62ed8a5bdb3a26d4e) +- 🔐 **File ownership in folder and knowledge attachments.** Attaching files to folders or knowledge bases now verifies per-file read access, and folder file lists in chat middleware are filtered to entries the caller can read, preventing unauthorized file content from being injected into RAG context. [Commit](https://github.com/open-webui/open-webui/commit/2dbf7b6764a7922458d3b0139687ad6dcd7596d9) +- 🔐 **Shared chat access for owners and admins.** Chat owners can now view and clone their own shared chats without requiring an explicit access grant, and administrators can manage shared chat access controls on any chat. [Commit](https://github.com/open-webui/open-webui/commit/3a21b334cce30226750c5c537345dc51bb8bef17), [Commit](https://github.com/open-webui/open-webui/commit/315566064aedeff071854b023d09e5f1ea0eb950) +- 🧵 **Legacy chat history self-healing.** Loading legacy conversations now automatically detects broken parent-link graphs in migrated message records, merges missing messages from the embedded JSON history, and backfills them to the normalized table so future loads use the fast path without data loss. [Commit](https://github.com/open-webui/open-webui/commit/1388f4568b8f508c26542673dd01f1fa049e798a) +- 🎛️ **Filter selector reactivity.** Model filter checkboxes now derive state reactively from the current filter list and selected IDs instead of capturing a one-time snapshot at mount, so checkboxes update correctly when model contexts or filter configurations change at runtime. [Commit](https://github.com/open-webui/open-webui/commit/d1ef5382377f590f97a6dbaee88f369e6d7c5f6f) +- 🌐 **Portuguese (Brazil) translation updates.** Translations for newly added UI items were added along with a consistency pass across existing entries. [#24503](https://github.com/open-webui/open-webui/pull/24503) ### Changed From c951b4f26226033e7f47d21292c125be4628fb74 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 11 May 2026 02:29:13 +0900 Subject: [PATCH 34/37] chore: format --- backend/open_webui/models/chats.py | 10 ++++------ backend/open_webui/routers/models.py | 15 ++++++--------- backend/open_webui/routers/tools.py | 4 +--- src/lib/components/chat/Artifacts.svelte | 5 ++++- .../chat/Messages/ContentRenderer.svelte | 6 +----- .../components/chat/Messages/UserMessage.svelte | 4 +++- src/lib/i18n/locales/ar-BH/translation.json | 2 ++ src/lib/i18n/locales/ar/translation.json | 2 ++ src/lib/i18n/locales/az-AZ/translation.json | 2 ++ src/lib/i18n/locales/bg-BG/translation.json | 2 ++ src/lib/i18n/locales/bn-BD/translation.json | 2 ++ src/lib/i18n/locales/bo-TB/translation.json | 2 ++ src/lib/i18n/locales/bs-BA/translation.json | 2 ++ src/lib/i18n/locales/ca-ES/translation.json | 2 ++ src/lib/i18n/locales/ceb-PH/translation.json | 2 ++ src/lib/i18n/locales/cs-CZ/translation.json | 2 ++ src/lib/i18n/locales/da-DK/translation.json | 2 ++ src/lib/i18n/locales/de-DE/translation.json | 2 ++ src/lib/i18n/locales/dg-DG/translation.json | 2 ++ src/lib/i18n/locales/el-GR/translation.json | 2 ++ src/lib/i18n/locales/en-GB/translation.json | 2 ++ src/lib/i18n/locales/en-US/translation.json | 2 +- src/lib/i18n/locales/es-ES/translation.json | 2 ++ src/lib/i18n/locales/et-EE/translation.json | 2 ++ src/lib/i18n/locales/eu-ES/translation.json | 2 ++ src/lib/i18n/locales/fa-IR/translation.json | 2 ++ src/lib/i18n/locales/fi-FI/translation.json | 2 ++ src/lib/i18n/locales/fil-PH/translation.json | 2 ++ src/lib/i18n/locales/fr-CA/translation.json | 2 ++ src/lib/i18n/locales/fr-FR/translation.json | 2 ++ src/lib/i18n/locales/gl-ES/translation.json | 2 ++ src/lib/i18n/locales/he-IL/translation.json | 2 ++ src/lib/i18n/locales/hi-IN/translation.json | 2 ++ src/lib/i18n/locales/hr-HR/translation.json | 2 ++ src/lib/i18n/locales/hu-HU/translation.json | 2 ++ src/lib/i18n/locales/id-ID/translation.json | 2 ++ src/lib/i18n/locales/ie-GA/translation.json | 2 ++ src/lib/i18n/locales/it-IT/translation.json | 2 ++ src/lib/i18n/locales/ja-JP/translation.json | 2 ++ src/lib/i18n/locales/ka-GE/translation.json | 2 ++ src/lib/i18n/locales/kab-DZ/translation.json | 2 ++ src/lib/i18n/locales/ko-KR/translation.json | 2 ++ src/lib/i18n/locales/lt-LT/translation.json | 2 ++ src/lib/i18n/locales/lv-LV/translation.json | 2 ++ src/lib/i18n/locales/ms-MY/translation.json | 2 ++ src/lib/i18n/locales/nb-NO/translation.json | 2 ++ src/lib/i18n/locales/nl-NL/translation.json | 2 ++ src/lib/i18n/locales/pa-IN/translation.json | 2 ++ src/lib/i18n/locales/pl-PL/translation.json | 2 ++ src/lib/i18n/locales/pt-BR/translation.json | 2 ++ src/lib/i18n/locales/pt-PT/translation.json | 2 ++ src/lib/i18n/locales/ro-RO/translation.json | 2 ++ src/lib/i18n/locales/ru-RU/translation.json | 2 ++ src/lib/i18n/locales/sk-SK/translation.json | 2 ++ src/lib/i18n/locales/sr-RS/translation.json | 2 ++ src/lib/i18n/locales/sv-SE/translation.json | 2 ++ src/lib/i18n/locales/ta-IN/translation.json | 2 ++ src/lib/i18n/locales/th-TH/translation.json | 2 ++ src/lib/i18n/locales/tk-TM/translation.json | 2 ++ src/lib/i18n/locales/tr-TR/translation.json | 2 ++ src/lib/i18n/locales/ug-CN/translation.json | 2 ++ src/lib/i18n/locales/uk-UA/translation.json | 2 ++ src/lib/i18n/locales/ur-PK/translation.json | 2 ++ src/lib/i18n/locales/uz-Cyrl-UZ/translation.json | 2 ++ src/lib/i18n/locales/uz-Latn-Uz/translation.json | 2 ++ src/lib/i18n/locales/vi-VN/translation.json | 2 ++ src/lib/i18n/locales/zh-CN/translation.json | 2 ++ src/lib/i18n/locales/zh-TW/translation.json | 2 ++ src/lib/utils/csp.ts | 4 +--- 69 files changed, 143 insertions(+), 29 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 51f2f121b3..957492d817 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -471,9 +471,7 @@ class ChatTable: if msg.get('parentId') and msg['parentId'] not in messages_map } - async def backfill_messages_by_chat_id( - self, chat_id: str, user_id: str, messages: dict[str, dict] - ) -> None: + async def backfill_messages_by_chat_id(self, chat_id: str, user_id: str, messages: dict[str, dict]) -> None: """Write messages to the ``chat_message`` table so future lookups use the fast path. Errors are logged but never raised. """ @@ -509,9 +507,9 @@ class ChatTable: # Graph has gaps — enrich from the legacy embedded history. log.info( - 'Chat %s: %d unresolved parent reference(s) in chat_message — ' - 'enriching from legacy history', - id, len(unresolved_ids), + 'Chat %s: %d unresolved parent reference(s) in chat_message — enriching from legacy history', + id, + len(unresolved_ids), ) chat = await self.get_chat_by_id(id) if chat: diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index cef4429760..2a78daa94d 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -425,15 +425,12 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes ) ) - if ( - write_access - or await AccessGrants.has_access( - user_id=user.id, - resource_type='model', - resource_id=model.id, - permission='read', - db=db, - ) + if write_access or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='read', + db=db, ): model_dict = model.model_dump() # Strip params (system prompt and other admin-curated config) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 3eddefdee6..cd11bcde5e 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -484,9 +484,7 @@ async def update_tools_by_id( if form_data.content != tools.content: if user.role != 'admin' and not ( await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db) - or await has_permission( - user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db - ) + or await has_permission(user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db) ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/src/lib/components/chat/Artifacts.svelte b/src/lib/components/chat/Artifacts.svelte index f8483d122e..f3ac74897d 100644 --- a/src/lib/components/chat/Artifacts.svelte +++ b/src/lib/components/chat/Artifacts.svelte @@ -244,7 +244,10 @@