mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
fix: verify chat ownership on /api/chat/completed and /api/chat/actions (#27486)
Both routes read `chat_id` from the request body and passed it into `get_event_emitter` without checking the caller owns that chat. The emitter persists through `upsert_message_to_chat_by_id_and_message_id`, which resolves by primary key and takes no owner argument, so an invoked filter or action wrote into whichever chat the caller named. `/api/chat/completions` already performs this check; these two routes did not. Adds `verify_chat_ownership`, called at the top of both handlers. It runs before the existing try block because the `except Exception` there catches HTTPException and would rewrite the 404 into a 400. Admins are exempt, matching the completions path, so deliberate cross-user operations keep working. `local:` chat ids are allowed through: they are per-socket, the emitter suppresses database writes for them, and the socket emit targets the caller's own room. `channel:` chat ids are rejected instead. They reach the channel emitter, whose write only checks that the message belongs to the channel and never that the caller may write it, and the membership and write-access gate for channels exists solely on `/api/chat/completions`. No caller sends a `channel:` id to these two routes: the only frontend callers are in the regular chat UI, and the backend channel path dispatches through the completions handler. Co-authored-by: manus-use <213290975+manus-use@users.noreply.github.com>
This commit is contained in:
@@ -1852,10 +1852,31 @@ async def generate_messages(
|
||||
return response
|
||||
|
||||
|
||||
async def verify_chat_ownership(chat_id: str | None, user) -> None:
|
||||
"""`local:` chats are per-socket and never persisted, so they have no owner to check."""
|
||||
if not chat_id or chat_id.startswith('local:'):
|
||||
return
|
||||
|
||||
# Channel messages need the membership and write-access gate that only /api/chat/completions has.
|
||||
if chat_id.startswith('channel:'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Channel chats are not supported on this endpoint',
|
||||
)
|
||||
|
||||
if user.role != 'admin' and not await Chats.is_chat_owner(chat_id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.DEFAULT(),
|
||||
)
|
||||
|
||||
|
||||
@app.post('/api/chat/completed')
|
||||
async def chat_completed(request: Request, form_data: dict, user=Depends(get_verified_user)):
|
||||
"""Deprecated: outlet filters now run inline during chat completion.
|
||||
Kept for backward compatibility with external integrations."""
|
||||
await verify_chat_ownership(form_data.get('chat_id'), user)
|
||||
|
||||
try:
|
||||
model_item = form_data.pop('model_item', {})
|
||||
|
||||
@@ -1873,6 +1894,8 @@ async def chat_completed(request: Request, form_data: dict, user=Depends(get_ver
|
||||
|
||||
@app.post('/api/chat/actions/{action_id}')
|
||||
async def chat_action(request: Request, action_id: str, form_data: dict, user=Depends(get_verified_user)):
|
||||
await verify_chat_ownership(form_data.get('chat_id'), user)
|
||||
|
||||
try:
|
||||
model_item = form_data.pop('model_item', {})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user