From e7ff4768f8ffe1924b4576381c9e45e8a64350e4 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:56:43 +0200 Subject: [PATCH] fix: Add ownership checks to global task endpoints (#23454) * Add ownership checks to global task endpoints - Restrict GET /api/tasks and POST /api/tasks/stop/{task_id} to admin-only - Add new scoped POST /api/tasks/chat/{chat_id}/stop endpoint with ownership check so regular users can stop their own chat tasks - Allow admins to access the scoped chat task endpoints alongside owners - Update frontend to use the new scoped stop endpoint when a chatId is available https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc * Handle temporary (local:) chat IDs in scoped task endpoints Temporary chats use local: as chat_id which doesn't exist in the DB. The scoped endpoints now skip ownership checks for local: IDs (they aren't enumerable) and use {chat_id:path} to handle the colon in the URL path. https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc * Verify session ownership for local: chat IDs and URL-encode chat_id - For local: chat IDs, look up the socket's owner in SESSION_POOL and verify it matches the requesting user (or admin) - URL-encode chat_id in frontend fetch calls to handle special characters (colon in local: IDs) safely https://claude.ai/code/session_01K7zPDvvjRu8AxJ4Br2HhZc --------- Co-authored-by: Claude --- backend/open_webui/main.py | 35 ++++++++++++++++++++++++----- src/lib/apis/index.ts | 34 +++++++++++++++++++++++++++- src/lib/components/chat/Chat.svelte | 12 ++++++++-- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index b0d0b3eab..716d0c92a 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -67,6 +67,7 @@ from open_webui.socket.main import ( periodic_session_pool_cleanup, get_event_emitter, get_models_in_use, + get_user_id_from_session_pool, ) from open_webui.routers import ( analytics, @@ -566,6 +567,7 @@ from open_webui.tasks import ( list_task_ids_by_item_id, create_task, stop_task, + stop_item_tasks, list_tasks, ) # Import from tasks.py @@ -1974,7 +1976,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user=De @app.post('/api/tasks/stop/{task_id}') -async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_verified_user)): +async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_admin_user)): try: result = await stop_task(request.app.state.redis, task_id) return result @@ -1983,15 +1985,21 @@ async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_ve @app.get('/api/tasks') -async def list_tasks_endpoint(request: Request, user=Depends(get_verified_user)): +async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)): return {'tasks': await list_tasks(request.app.state.redis)} -@app.get('/api/tasks/chat/{chat_id}') +@app.get('/api/tasks/chat/{chat_id:path}') async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): - chat = await Chats.get_chat_by_id(chat_id) - if chat is None or chat.user_id != user.id: - return {'task_ids': []} + if chat_id.startswith('local:'): + socket_id = chat_id[len('local:'):] + owner_id = get_user_id_from_session_pool(socket_id) + if owner_id != user.id and user.role != 'admin': + return {'task_ids': []} + else: + chat = await Chats.get_chat_by_id(chat_id) + if chat is None or (chat.user_id != user.id and user.role != 'admin'): + return {'task_ids': []} task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id) @@ -1999,6 +2007,21 @@ async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De return {'task_ids': task_ids} +@app.post('/api/tasks/chat/{chat_id:path}/stop') +async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): + if chat_id.startswith('local:'): + socket_id = chat_id[len('local:'):] + owner_id = get_user_id_from_session_pool(socket_id) + if owner_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + else: + chat = await Chats.get_chat_by_id(chat_id) + if chat is None or (chat.user_id != user.id and user.role != 'admin'): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + result = await stop_item_tasks(request.app.state.redis, chat_id) + return result + + ################################## # # Config Endpoints diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index 05475b4b3..5faf56d4d 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -273,10 +273,42 @@ export const stopTask = async (token: string, id: string) => { return res; }; +export const stopTasksByChatId = async (token: string, chat_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${encodeURIComponent(chat_id)}/stop`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getTaskIdsByChatId = async (token: string, chat_id: string) => { let error = null; - const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${chat_id}`, { + const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${encodeURIComponent(chat_id)}`, { method: 'GET', headers: { Accept: 'application/json', diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 6903429f7..7e69b4512 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -86,6 +86,7 @@ chatAction, generateMoACompletion, stopTask, + stopTasksByChatId, getTaskIdsByChatId } from '$lib/apis'; import { getTools } from '$lib/apis/tools'; @@ -2464,11 +2465,18 @@ const stopResponse = async (processQueue = true) => { if (taskIds) { - for (const taskId of taskIds) { - const res = await stopTask(localStorage.token, taskId).catch((error) => { + if ($chatId) { + await stopTasksByChatId(localStorage.token, $chatId).catch((error) => { toast.error(`${error}`); return null; }); + } else { + for (const taskId of taskIds) { + const res = await stopTask(localStorage.token, taskId).catch((error) => { + toast.error(`${error}`); + return null; + }); + } } taskIds = null;