Skip to content

Commit e7ff476

Browse files
Classic298claude
andauthored
fix: Add ownership checks to global task endpoints (open-webui#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:<socketId> 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:<socketId> 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 <noreply@anthropic.com>
1 parent c47dd7b commit e7ff476

3 files changed

Lines changed: 72 additions & 9 deletions

File tree

backend/open_webui/main.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
periodic_session_pool_cleanup,
6868
get_event_emitter,
6969
get_models_in_use,
70+
get_user_id_from_session_pool,
7071
)
7172
from open_webui.routers import (
7273
analytics,
@@ -566,6 +567,7 @@
566567
list_task_ids_by_item_id,
567568
create_task,
568569
stop_task,
570+
stop_item_tasks,
569571
list_tasks,
570572
) # Import from tasks.py
571573

@@ -1974,7 +1976,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user=De
19741976

19751977

19761978
@app.post('/api/tasks/stop/{task_id}')
1977-
async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_verified_user)):
1979+
async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_admin_user)):
19781980
try:
19791981
result = await stop_task(request.app.state.redis, task_id)
19801982
return result
@@ -1983,22 +1985,43 @@ async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_ve
19831985

19841986

19851987
@app.get('/api/tasks')
1986-
async def list_tasks_endpoint(request: Request, user=Depends(get_verified_user)):
1988+
async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)):
19871989
return {'tasks': await list_tasks(request.app.state.redis)}
19881990

19891991

1990-
@app.get('/api/tasks/chat/{chat_id}')
1992+
@app.get('/api/tasks/chat/{chat_id:path}')
19911993
async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
1992-
chat = await Chats.get_chat_by_id(chat_id)
1993-
if chat is None or chat.user_id != user.id:
1994-
return {'task_ids': []}
1994+
if chat_id.startswith('local:'):
1995+
socket_id = chat_id[len('local:'):]
1996+
owner_id = get_user_id_from_session_pool(socket_id)
1997+
if owner_id != user.id and user.role != 'admin':
1998+
return {'task_ids': []}
1999+
else:
2000+
chat = await Chats.get_chat_by_id(chat_id)
2001+
if chat is None or (chat.user_id != user.id and user.role != 'admin'):
2002+
return {'task_ids': []}
19952003

19962004
task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id)
19972005

19982006
log.debug(f'Task IDs for chat {chat_id}: {task_ids}')
19992007
return {'task_ids': task_ids}
20002008

20012009

2010+
@app.post('/api/tasks/chat/{chat_id:path}/stop')
2011+
async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
2012+
if chat_id.startswith('local:'):
2013+
socket_id = chat_id[len('local:'):]
2014+
owner_id = get_user_id_from_session_pool(socket_id)
2015+
if owner_id != user.id and user.role != 'admin':
2016+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
2017+
else:
2018+
chat = await Chats.get_chat_by_id(chat_id)
2019+
if chat is None or (chat.user_id != user.id and user.role != 'admin'):
2020+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)
2021+
result = await stop_item_tasks(request.app.state.redis, chat_id)
2022+
return result
2023+
2024+
20022025
##################################
20032026
#
20042027
# Config Endpoints

src/lib/apis/index.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,42 @@ export const stopTask = async (token: string, id: string) => {
273273
return res;
274274
};
275275

276+
export const stopTasksByChatId = async (token: string, chat_id: string) => {
277+
let error = null;
278+
279+
const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${encodeURIComponent(chat_id)}/stop`, {
280+
method: 'POST',
281+
headers: {
282+
Accept: 'application/json',
283+
'Content-Type': 'application/json',
284+
...(token && { authorization: `Bearer ${token}` })
285+
}
286+
})
287+
.then(async (res) => {
288+
if (!res.ok) throw await res.json();
289+
return res.json();
290+
})
291+
.catch((err) => {
292+
console.error(err);
293+
if ('detail' in err) {
294+
error = err.detail;
295+
} else {
296+
error = err;
297+
}
298+
return null;
299+
});
300+
301+
if (error) {
302+
throw error;
303+
}
304+
305+
return res;
306+
};
307+
276308
export const getTaskIdsByChatId = async (token: string, chat_id: string) => {
277309
let error = null;
278310

279-
const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${chat_id}`, {
311+
const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${encodeURIComponent(chat_id)}`, {
280312
method: 'GET',
281313
headers: {
282314
Accept: 'application/json',

src/lib/components/chat/Chat.svelte

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
chatAction,
8787
generateMoACompletion,
8888
stopTask,
89+
stopTasksByChatId,
8990
getTaskIdsByChatId
9091
} from '$lib/apis';
9192
import { getTools } from '$lib/apis/tools';
@@ -2464,11 +2465,18 @@
24642465
24652466
const stopResponse = async (processQueue = true) => {
24662467
if (taskIds) {
2467-
for (const taskId of taskIds) {
2468-
const res = await stopTask(localStorage.token, taskId).catch((error) => {
2468+
if ($chatId) {
2469+
await stopTasksByChatId(localStorage.token, $chatId).catch((error) => {
24692470
toast.error(`${error}`);
24702471
return null;
24712472
});
2473+
} else {
2474+
for (const taskId of taskIds) {
2475+
const res = await stopTask(localStorage.token, taskId).catch((error) => {
2476+
toast.error(`${error}`);
2477+
return null;
2478+
});
2479+
}
24722480
}
24732481
24742482
taskIds = null;

0 commit comments

Comments
 (0)