Skip to content

fix(security): scope task + per-user REST routes to the caller's projects (WPScan #42604, #43987) - #660

Merged
arifulhoque7 merged 1 commit into
weDevsOfficial:developfrom
arifulhoque7:fix/security-cross-project-task-access
Aug 19, 2026
Merged

arifulhoque7 merged 1 commit into
weDevsOfficial:developfrom
arifulhoque7:fix/security-cross-project-task-access

Conversation

@arifulhoque7

Copy link
Copy Markdown
Contributor

Refs weDevsOfficial/pm-pro#461 (WPScan #42604 and #43987).

Two of the three reports in #461 are cross-project data-access defects in the Free plugin: routes gated only by Authentic (logged-in only), with no constraint tying the query/ids to the projects the caller belongs to. This PR closes both. The third report (#45258, unauthenticated account creation) is handled by #658 and is not touched here.

#42604 — cross-project task disclosure & modification

tasks, advanced/tasks, advanced/taskscsv run Task::get_results(), which only constrains by project when a project_id param is present. The one hook that could add a membership constraint, wedevs_pm_task_where, has no registered listener — so a subscriber belonging to no project read every project's tasks, their descriptions and the creators'/assignees' email addresses, and exported the same set as CSV.

  • Added where_membership() to the query builder. Managers/admins query across projects as before; everyone else is bound to the projects they are a member of (a member of none matches nothing). Applied in the shared where() chain, so every caller of get_results() (tasks, advanced tasks, CSV, kanban) is covered. projects/{project_id}/tasks (index) already scopes by project_id on its own query and is unaffected.

projects/{project_id}/tasks/sorting accepted list_id / task_id / orders[].id from the body while checking only the project in the URL, so a member of one project could reorder and relocate another project's tasks.

  • Now requires Access_Project, and binds every list/task id in the body to that project_id before mutating (rejects with 403 on mismatch).

#43987 — any user's activity feed, tasks & email

users/{id}/user-activities and users/{id}/tasks used the {id} from the URL with no check that it is the caller, leaking the target's activity feed (with actor email) and task list (with the target's user object).

  • New Access_User permission: a caller may read only their own resources; reading another user's requires a project-management capability. Applied to user-activities, users/{id}/tasks, and users/{id}/tasks/calender.
  • assigned_users already scopes to the caller's projects and is left unchanged.

The property

The named routes are the reported instances of one property: every route in this namespace must constrain its query to the caller's projects, and bind any request-supplied id to the project it gated on. Access_Project and the scoped File_Controller::index already follow it; this PR extends it to the task and per-user routes.

Non-breaking

  • My Tasks always requests the current user's id (PM_Vars.current_user.ID) → Access_User passes.
  • Reports / advanced views are manager-facing; the manage capability bypasses the membership clause, so admins still see all tasks.
  • Per-project managers are members in pm_role_user, so their own projects remain visible.

Verification

  • php -l clean on all changed files.
  • No line-ending changes (Free plugin is CRLF; diff is content-only).

Files

  • core/Permissions/Access_User.php (new)
  • routes/mytask.phpuser-activities / tasks / tasks/calenderAccess_User
  • routes/task.phptasks/sortingAccess_Project
  • src/Task/Helper/Task.phpwhere_membership()
  • src/Task/Controllers/Task_Controller.php — bind sorting ids to project

Closes cross-project data-access reports in WPScan #42604 and #43987
(WP Project Manager <= 4.0.6). Both let any logged-in user read or modify
data outside the projects they belong to, because these routes were gated
by Authentic (logged-in only) with no membership constraint.

#42604 - cross-project task disclosure and modification
- tasks / advanced/tasks / advanced/taskscsv run Task::get_results(),
  which only constrains by project when a project_id param is present.
  The intended hook (wedevs_pm_task_where) had no listener, so a member
  of no project read every project's tasks, descriptions and the
  creators'/assignees' emails, and exported them as CSV. Added
  where_membership() to the query builder: managers/admins query across
  projects as before, everyone else is bound to the projects they are a
  member of (a member of none sees nothing).
- projects/{project_id}/tasks/sorting accepted task and list ids in the
  body while gating only the project in the URL, so a member of one
  project could reorder/relocate another project's tasks. It now requires
  Access_Project and binds every list/task id in the body to that project.

#43987 - any user's activity feed, tasks and email
- users/{id}/user-activities and users/{id}/tasks used the {id} from the
  URL with no check that it is the caller. New Access_User permission
  allows a caller to read only their own resources; reading another
  user's requires a project-management capability. Applied to those two
  routes plus users/{id}/tasks/calender.

The named routes are the reported instances of one property: every route
in this namespace must constrain its query to the caller's projects and
bind request ids to the project it gated on. assigned_users already
scopes correctly and is unchanged.

Non-breaking: the My Tasks screen always requests the current user's id,
and reports/advanced views are manager-facing (manage cap bypasses the
membership clause). No line-ending or unrelated changes.
@arifulhoque7
arifulhoque7 merged commit f69f12e into weDevsOfficial:develop Aug 19, 2026
3 checks passed
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.

1 participant