fix: prevent cross-user workspace leak via stale cookie
- _get_active_workspace now verifies workspace owner matches current user - Fallback: only picks workspace owned by current user (not any user) - /local-workspace redirects to /workspaces when no workspace exists - New users no longer see other users' workspaces/projects
This commit is contained in:
+29
-17
@@ -581,9 +581,13 @@ async def local_workspace_page(request: Request, folder: int = None):
|
||||
if not user:
|
||||
return RedirectResponse("/auth/login?provider=local", status_code=302)
|
||||
|
||||
ws = _get_active_workspace(request)
|
||||
ws = _get_active_workspace(request, user_id=user["id"])
|
||||
ws_id = ws["id"] if ws else None
|
||||
|
||||
# If no workspace exists for this user, redirect to workspaces page
|
||||
if not ws_id:
|
||||
return RedirectResponse("/workspaces", status_code=302)
|
||||
|
||||
# Build breadcrumb if navigating into a folder
|
||||
breadcrumb = []
|
||||
current_folder_id = folder
|
||||
@@ -617,7 +621,7 @@ async def local_workspace_tree(request: Request, folder: int = None):
|
||||
If ?folder=ID is provided, returns only that folder's children.
|
||||
Otherwise returns the full recursive tree from root.
|
||||
"""
|
||||
ws = _get_active_workspace(request)
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
ws_id = ws["id"] if ws else None
|
||||
if not ws_id:
|
||||
return {"tree": [], "breadcrumb": []}
|
||||
@@ -777,7 +781,7 @@ async def create_local_workspace_item(request: Request):
|
||||
item_type = body.get("type", "page")
|
||||
parent_id = body.get("parent_id")
|
||||
explicit_ws_id = body.get("workspace_id")
|
||||
ws = _get_active_workspace(request)
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
ws_id = explicit_ws_id or (ws["id"] if ws else None)
|
||||
ws_key = ws["workspace_key"] if ws and not explicit_ws_id else ""
|
||||
section = 'Workspace' if item_type == 'folder' else 'Private'
|
||||
@@ -867,7 +871,7 @@ async def upload_local_workspace_file(request: Request):
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ws = _get_active_workspace(request)
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
ws_id = ws["id"] if ws else None
|
||||
if not ws_id:
|
||||
return JSONResponse({"error": "No active workspace"}, status_code=400)
|
||||
@@ -940,7 +944,7 @@ async def upload_local_workspace_folder(request: Request):
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ws = _get_active_workspace(request)
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
ws_id = ws["id"] if ws else None
|
||||
if not ws_id:
|
||||
return JSONResponse({"error": "No active workspace"}, status_code=400)
|
||||
@@ -1043,22 +1047,30 @@ async def upload_local_workspace_folder(request: Request):
|
||||
WORKSPACE_COOKIE = "flowdeck_workspace"
|
||||
|
||||
|
||||
def _get_active_workspace(request: Request) -> dict | None:
|
||||
"""Get the active workspace ID from the cookie, or first workspace, or None."""
|
||||
def _get_active_workspace(request: Request, user_id: int = None) -> dict | None:
|
||||
"""Get the active workspace ID from the cookie (verified for current user), or first user workspace, or None."""
|
||||
ws_id = request.cookies.get(WORKSPACE_COOKIE)
|
||||
if ws_id:
|
||||
try:
|
||||
with get_conn() as conn:
|
||||
ws = conn.execute("SELECT * FROM workspaces WHERE id=?", (int(ws_id),)).fetchone()
|
||||
ws = conn.execute("SELECT * FROM workspaces WHERE id=?",
|
||||
(int(ws_id),)).fetchone()
|
||||
if ws:
|
||||
return dict(ws)
|
||||
ws_dict = dict(ws)
|
||||
# Verify ownership — only return if it belongs to the current user
|
||||
if user_id is None or ws_dict.get("owner_id") == user_id:
|
||||
return ws_dict
|
||||
except (ValueError, Exception):
|
||||
pass
|
||||
# Fallback: first workspace
|
||||
with get_conn() as conn:
|
||||
ws = conn.execute("SELECT * FROM workspaces ORDER BY id LIMIT 1").fetchone()
|
||||
if ws:
|
||||
return dict(ws)
|
||||
# Fallback: first workspace owned by this user
|
||||
if user_id:
|
||||
with get_conn() as conn:
|
||||
ws = conn.execute(
|
||||
"SELECT * FROM workspaces WHERE owner_id=? ORDER BY id LIMIT 1",
|
||||
(user_id,)
|
||||
).fetchone()
|
||||
if ws:
|
||||
return dict(ws)
|
||||
return None
|
||||
|
||||
|
||||
@@ -1091,7 +1103,7 @@ async def list_workspaces(request: Request):
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
workspaces.append(d)
|
||||
active = _get_active_workspace(request)
|
||||
active = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
return {"workspaces": workspaces, "active_id": active["id"] if active else None}
|
||||
|
||||
|
||||
@@ -1310,7 +1322,7 @@ async def list_all_tags_global(request: Request):
|
||||
@router.get("/api/local-workspace/tags")
|
||||
async def list_tags(request: Request):
|
||||
"""List tags used in the active workspace (current user's tags only)."""
|
||||
ws = _get_active_workspace(request)
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
ws_id = ws["id"] if ws else None
|
||||
uid = _get_user_id(request)
|
||||
if not ws_id:
|
||||
@@ -1392,7 +1404,7 @@ async def remove_item_tag(item_id: int, tag_id: int):
|
||||
@router.get("/api/local-workspace/tags/search")
|
||||
async def search_by_tags(request: Request, tags: str = ""):
|
||||
"""Search items by tags (comma-separated)."""
|
||||
ws = _get_active_workspace(request)
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
ws_id = ws["id"] if ws else None
|
||||
if not ws_id:
|
||||
return {"items": []}
|
||||
|
||||
Reference in New Issue
Block a user