From 736e002935a58ec72568536175e0e180ec02f0bf Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:34:29 +0200 Subject: [PATCH] Fail closed if tool-server config does not persist to disk set_tool_servers_config assigned TOOL_SERVER_CONNECTIONS via AppConfig.__setattr__, whose DB write is fire-and-forget and swallows errors, then returned 200 without confirming it landed. Add AppConfig.commit(name) to await the durable write and raise on failure, and call it after the assignment so a persistence failure returns HTTP 500 instead of a silent success. Hardening only: a config write fails only when the database is inaccessible, in which case Open WebUI cannot run at all (it migrates and reads that database at startup and on every request), so this cannot occur on a live instance an attacker could reach. Co-authored-by: zharise <50876147+zharise@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/open_webui/internal/config.py | 5 +++++ backend/open_webui/routers/configs.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/backend/open_webui/internal/config.py b/backend/open_webui/internal/config.py index 46f5b1b67d..7b18c362da 100644 --- a/backend/open_webui/internal/config.py +++ b/backend/open_webui/internal/config.py @@ -233,6 +233,11 @@ class AppConfig: except Exception as exc: log.error("Async persist failed for '%s': %s", name, exc) + async def commit(self, name: str) -> None: + """Await the durable DB write of one entry, raising on failure (__setattr__'s write is fire-and-forget).""" + if _persist_enabled: + await self._entries[name].commit_async() + def __getattr__(self, name: str) -> Any: entries = super().__getattribute__('_entries') if name not in entries: diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index c40131f41f..f2ef22bea3 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -197,6 +197,14 @@ async def set_tool_servers_config( connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS ] + # The write above is fire-and-forget and swallows errors; confirm it reached disk so a + # persistence failure fails closed (HTTP 500) instead of returning a silent success. + try: + await request.app.state.config.commit('TOOL_SERVER_CONNECTIONS') + except Exception as e: + log.error(f'Failed to persist tool server connections: {e}') + raise HTTPException(status_code=500, detail='Failed to persist tool server configuration') + await set_tool_servers(request) for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: