Skip to content

security: enforce user ownership on global tasks (Fix IDOR)#1065

Open
Adraca wants to merge 2 commits intodfir-iris:masterfrom
Adraca:fix/tasks-idor
Open

security: enforce user ownership on global tasks (Fix IDOR)#1065
Adraca wants to merge 2 commits intodfir-iris:masterfrom
Adraca:fix/tasks-idor

Conversation

@Adraca
Copy link
Copy Markdown

@Adraca Adraca commented Apr 22, 2026

Ensures that global tasks can only be viewed, updated, or deleted by the task owner or assignee. Linked to issue #1034.

Summary by CodeRabbit

  • Bug Fixes
    • Viewing, editing, and deleting global tasks now only succeeds when the current user is the task opener or the task assignee. Attempts by other users will return the same not-found/invalid responses as before and will not perform any changes, ensuring task actions are limited to relevant users.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8396df66-8a78-4187-80bc-70992bb35d3b

📥 Commits

Reviewing files that changed from the base of the PR and between ff59187 and 8490c23.

📒 Files selected for processing (1)
  • source/app/blueprints/rest/dashboard_routes.py
✅ Files skipped from review due to trivial changes (1)
  • source/app/blueprints/rest/dashboard_routes.py

Walkthrough

Added ownership/assignee filtering to GlobalTasks GET/EDIT/DELETE handlers so queries and deletions only match tasks where task_userid_open == current_user.id or task_assignee_id == current_user.id.

Changes

Cohort / File(s) Summary
Global Task Access Control
source/app/blueprints/rest/dashboard_routes.py
Applied ownership/assignee filter (task_userid_open == current_user.id OR task_assignee_id == current_user.id) to the /global/tasks/<int:cur_id> GET, edit_gtask, and gtask_delete handlers so retrieval, edit, and delete operations only affect tasks accessible to the current user.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • Issue #1034: Implements ownership/assignee filtering to mitigate the reported IDOR on global task view/update/delete endpoints.

Poem

🐰 I hop through code with careful paws,
I mark the tasks that fit the laws.
If you're the opener or the one assigned,
Only then will the task be signed—
Secure and snug, no gaps to find.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: enforcing user ownership constraints on global tasks to fix an IDOR vulnerability.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
source/app/blueprints/rest/dashboard_routes.py (1)

157-164: ⚠️ Potential issue | 🟠 Major

IDOR still present on the view endpoint.

The PR description states that global tasks should only be viewed, updated, or deleted by the owner or assignee. However, view_gtask (GET /global/tasks/<cur_id>) returns any task by id via get_global_task(task_id=cur_id) regardless of ownership. Meanwhile, the update and delete endpoints (edit_gtask and gtask_delete) both enforce the required filter:

or_(GlobalTasks.task_userid_open == current_user.id, GlobalTasks.task_assignee_id == current_user.id)

Any authenticated user can currently read any global task's details—the same IDOR class being fixed for update/delete. Since get_global_task is only called from view_gtask, you can either apply the filter inline (as sketched below) or push it into the helper for consistency with the other endpoints.

In-route fix (inline filter)
 `@dashboard_rest_blueprint.route`('/global/tasks/<int:cur_id>', methods=['GET'])
 `@ac_api_requires`()
 def view_gtask(cur_id):
-    task = get_global_task(task_id=cur_id)
-    if not task:
+    task = get_global_task(task_id=cur_id)
+    if not task or (task.task_userid_open != current_user.id
+                    and task.task_assignee_id != current_user.id):
         return response_error(f'Global task ID {cur_id} not found')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/app/blueprints/rest/dashboard_routes.py` around lines 157 - 164,
view_gtask currently returns any global task by id causing an IDOR; enforce the
same owner/assignee restriction used by edit_gtask and gtask_delete by filtering
so only tasks where GlobalTasks.task_userid_open == current_user.id OR
GlobalTasks.task_assignee_id == current_user.id are returned. Fix by updating
view_gtask to call get_global_task with that filter (or add the filter logic
into get_global_task itself) so the GET /global/tasks/<cur_id> only returns the
task when the current_user is owner or assignee.
🧹 Nitpick comments (1)
source/app/blueprints/rest/dashboard_routes.py (1)

288-293: Redundant query in delete path; factor out the duplicated ownership filter.

gtask_delete runs the ownership-scoped query twice — once to fetch (line 288) and once to delete (line 292). Since data is already loaded, just delete that instance; this also makes the intent clearer and removes a class of subtle bugs (e.g., if the two filters ever drift). While here, the same or_(...) expression appears three times in this file — extracting it into a small helper or scoping it on a query object reduces the risk of an inconsistent fix later.

♻️ Proposed refactor
-    data = GlobalTasks.query.filter(GlobalTasks.id == cur_id).filter(or_(GlobalTasks.task_userid_open == current_user.id, GlobalTasks.task_assignee_id == current_user.id)).first()
-    if not data:
-        return response_error("Invalid global task ID")
-
-    GlobalTasks.query.filter(GlobalTasks.id == cur_id).filter(or_(GlobalTasks.task_userid_open == current_user.id, GlobalTasks.task_assignee_id == current_user.id)).delete()
-    db.session.commit()
+    data = GlobalTasks.query.filter(
+        GlobalTasks.id == cur_id,
+        or_(GlobalTasks.task_userid_open == current_user.id,
+            GlobalTasks.task_assignee_id == current_user.id),
+    ).first()
+    if not data:
+        return response_error("Invalid global task ID")
+
+    db.session.delete(data)
+    db.session.commit()

For the duplication, consider a small helper next to the model/data-access layer, e.g.:

def _user_can_access_gtask_filter():
    return or_(
        GlobalTasks.task_userid_open == current_user.id,
        GlobalTasks.task_assignee_id == current_user.id,
    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/app/blueprints/rest/dashboard_routes.py` around lines 288 - 293, The
delete path duplicates the ownership filter and re-queries instead of deleting
the already loaded instance; update gtask_delete to reuse the previously fetched
data (variable data) when deleting rather than issuing a second filtered delete,
and extract the repeated or_(GlobalTasks.task_userid_open == current_user.id,
GlobalTasks.task_assignee_id == current_user.id) into a small helper (e.g.,
_user_can_access_gtask_filter) or a query-scoping function so both the initial
lookup (GlobalTasks.query.filter(...).first()) and any future checks use the
same filter helper to avoid drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@source/app/blueprints/rest/dashboard_routes.py`:
- Line 245: Import sqlalchemy.or_ into the module and apply ownership filtering
wherever tasks are fetched: update the imports to include or_ and change queries
that fetch GlobalTasks (e.g., the one in GlobalTasks.query.filter(...) used in
the update and delete flows) to use or_(GlobalTasks.task_userid_open ==
current_user.id, GlobalTasks.task_assignee_id == current_user.id). Also fix the
view_gtask/get_global_task flow by either adding the same ownership filters
inside get_global_task or by applying the ownership filter in view_gtask before
returning the task (ensure get_global_task(task_id=cur_id) cannot return tasks
the current_user does not own), and simplify delete to use that filtered query
instead of re-querying redundantly.

---

Outside diff comments:
In `@source/app/blueprints/rest/dashboard_routes.py`:
- Around line 157-164: view_gtask currently returns any global task by id
causing an IDOR; enforce the same owner/assignee restriction used by edit_gtask
and gtask_delete by filtering so only tasks where GlobalTasks.task_userid_open
== current_user.id OR GlobalTasks.task_assignee_id == current_user.id are
returned. Fix by updating view_gtask to call get_global_task with that filter
(or add the filter logic into get_global_task itself) so the GET
/global/tasks/<cur_id> only returns the task when the current_user is owner or
assignee.

---

Nitpick comments:
In `@source/app/blueprints/rest/dashboard_routes.py`:
- Around line 288-293: The delete path duplicates the ownership filter and
re-queries instead of deleting the already loaded instance; update gtask_delete
to reuse the previously fetched data (variable data) when deleting rather than
issuing a second filtered delete, and extract the repeated
or_(GlobalTasks.task_userid_open == current_user.id,
GlobalTasks.task_assignee_id == current_user.id) into a small helper (e.g.,
_user_can_access_gtask_filter) or a query-scoping function so both the initial
lookup (GlobalTasks.query.filter(...).first()) and any future checks use the
same filter helper to avoid drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 13f794b5-9c6c-4389-81ba-6ffce94f8607

📥 Commits

Reviewing files that changed from the base of the PR and between a4bfeda and ff59187.

📒 Files selected for processing (1)
  • source/app/blueprints/rest/dashboard_routes.py

def edit_gtask(cur_id, caseid):
form = CaseGlobalTaskForm()
task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).first()
task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).filter(or_(GlobalTasks.task_userid_open == current_user.id, GlobalTasks.task_assignee_id == current_user.id)).first()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm `or_` is not imported in the file and that the new code references it.
rg -nP '\bor_\b' source/app/blueprints/rest/dashboard_routes.py
rg -nP '^\s*(from|import)\b.*\bor_\b' source/app/blueprints/rest/dashboard_routes.py

Repository: dfir-iris/iris-web

Length of output: 607


🏁 Script executed:

# Get the view_gtask function and surrounding context (lines 157-164)
sed -n '155,170p' source/app/blueprints/rest/dashboard_routes.py

# Get the gtask_delete function and surrounding context (lines 280-295)
sed -n '278,296p' source/app/blueprints/rest/dashboard_routes.py

# Also check imports at the top of the file to confirm no or_ import
sed -n '1,60p' source/app/blueprints/rest/dashboard_routes.py

Repository: dfir-iris/iris-web

Length of output: 3926


🏁 Script executed:

# Find and inspect the get_global_task function to see if it includes ownership filters
fd -n 'dashboard_db.py' source/

Repository: dfir-iris/iris-web

Length of output: 292


🏁 Script executed:

# Find dashboard_db.py
fd dashboard_db.py source/

# Then inspect the get_global_task function

Repository: dfir-iris/iris-web

Length of output: 107


🏁 Script executed:

# Search for get_global_task function definition and its implementation
rg -A 10 'def get_global_task' source/app/datamgmt/dashboard/dashboard_db.py

Repository: dfir-iris/iris-web

Length of output: 456


🏁 Script executed:

# Get more context of get_global_task function
rg -A 25 'def get_global_task' source/app/datamgmt/dashboard/dashboard_db.py

Repository: dfir-iris/iris-web

Length of output: 739


Critical: or_ is not imported — endpoints will raise NameError at runtime.

The new ownership filters at lines 245, 288, and 292 all reference or_, but sqlalchemy.or_ is not imported anywhere in this module. As soon as a user calls POST /global/tasks/update/<id> or POST /global/tasks/delete/<id>, Flask will raise NameError: name 'or_' is not defined and return a 500 — so the IDOR fix not only doesn't land, it also breaks legitimate edit/delete flows.

Additionally, the view_gtask endpoint (lines 157-164) is still vulnerable to IDOR. It calls get_global_task(task_id=cur_id), which only filters by task ID with no ownership check. Any user can retrieve details of any global task regardless of whether they created it or are assigned to it.

🛠️ Proposed fixes

Add missing import:

 import marshmallow
 from datetime import datetime
 from datetime import timedelta
 from oic.oauth2.exception import GrantError

+from sqlalchemy import or_
+
 from flask import Blueprint

Apply ownership filter in view_gtask:

 def view_gtask(cur_id):
-    task = get_global_task(task_id=cur_id)
+    task = GlobalTasks.query.filter(GlobalTasks.id == cur_id).filter(or_(GlobalTasks.task_userid_open == current_user.id, GlobalTasks.task_assignee_id == current_user.id)).first()
     if not task:
         return response_error(f'Global task ID {cur_id} not found')

Minor refactor — avoid redundant query in delete:

     data = GlobalTasks.query.filter(GlobalTasks.id == cur_id).filter(or_(GlobalTasks.task_userid_open == current_user.id, GlobalTasks.task_assignee_id == current_user.id)).first()
     if not data:
         return response_error("Invalid global task ID")

-    GlobalTasks.query.filter(GlobalTasks.id == cur_id).filter(or_(GlobalTasks.task_userid_open == current_user.id, GlobalTasks.task_assignee_id == current_user.id)).delete()
+    db.session.delete(data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/app/blueprints/rest/dashboard_routes.py` at line 245, Import
sqlalchemy.or_ into the module and apply ownership filtering wherever tasks are
fetched: update the imports to include or_ and change queries that fetch
GlobalTasks (e.g., the one in GlobalTasks.query.filter(...) used in the update
and delete flows) to use or_(GlobalTasks.task_userid_open == current_user.id,
GlobalTasks.task_assignee_id == current_user.id). Also fix the
view_gtask/get_global_task flow by either adding the same ownership filters
inside get_global_task or by applying the ownership filter in view_gtask before
returning the task (ensure get_global_task(task_id=cur_id) cannot return tasks
the current_user does not own), and simplify delete to use that filtered query
instead of re-querying redundantly.

@Adraca
Copy link
Copy Markdown
Author

Adraca commented Apr 22, 2026

Updated the PR to include missing imports and addressed the IDOR in the view endpoint as flagged by the review. Ready for re-audit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants