fix(tasks): scope manage_tasks mutations to an exact task owner (#5264)

The edit/delete/pause/run actions of do_manage_tasks gated ownership with
`if owner and task.owner and task.owner != owner`. The middle term made the
check a no-op whenever task.owner was null/empty — the state a scheduled task
sits in when it was created in no-login mode (or via the localhost middleware
bypass) before the periodic legacy-owner sweep reassigns it to the admin user.
Any authenticated user's agent could then edit, delete, pause, or run another
tenant's owner-less task; edit+run lets an attacker rewrite the task prompt and
execute it in the scheduler's agent context.

The sibling `list` action already scopes with an exact `owner == owner` filter,
so the mutators were strictly more permissive than the reader. Drop the middle
term so the guard fails closed on owner-less rows for authenticated callers,
matching `list` and the calendar/notes/gallery/session null-owner gates. Auth
disabled (owner falsy) and same-owner access are unchanged.
This commit is contained in:
Ashvin
2026-07-11 06:15:14 +05:30
committed by GitHub
parent 4901a96591
commit a8d215a390
2 changed files with 147 additions and 4 deletions
+8 -4
View File
@@ -356,7 +356,11 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
return {"error": f"Task {task_id} not found", "exit_code": 1}
if owner and task.owner and task.owner != owner:
# Strict ownership: the old `task.owner and task.owner != owner`
# skipped the check on an owner-less task (created in no-login mode
# or before the legacy-owner sweep), letting any authenticated user
# reach it. `list` already scopes to an exact owner match.
if owner and task.owner != owner:
return {"error": "Access denied", "exit_code": 1}
changed = []
@@ -402,7 +406,7 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
return {"error": f"Task {task_id} not found", "exit_code": 1}
if owner and task.owner and task.owner != owner:
if owner and task.owner != owner:
return {"error": "Access denied", "exit_code": 1}
name = task.name
db.delete(task)
@@ -416,7 +420,7 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
return {"error": f"Task {task_id} not found", "exit_code": 1}
if owner and task.owner and task.owner != owner:
if owner and task.owner != owner:
return {"error": "Access denied", "exit_code": 1}
if action == "pause":
@@ -437,7 +441,7 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
if not task:
return {"error": f"Task {task_id} not found", "exit_code": 1}
if owner and task.owner and task.owner != owner:
if owner and task.owner != owner:
return {"error": "Access denied", "exit_code": 1}
from src.event_bus import get_task_scheduler