#5874·kanboard

Revoked project members keep reading assigned tasks via getMyDashboard/getMyOverdueTasks (no membership filter in getUserQuery)

Author: ybsun0215Created Aug 15, 2026Updated Aug 15, 2026

Environment

Kanboard v1.2.53 (Docker image kanboard/kanboard:v1.2.53), JSON-RPC API (POST /jsonrpc.php, HTTP Basic auth).

Summary

After a user is removed from a project, the API procedures getMyDashboard and getMyOverdueTasks keep returning the full task records (title, description, due dates, private project name) of that project's tasks which are assigned to the removed user, while direct access (getTask) is correctly denied. The leak is permanent because membership removal does not clear task assignments.

Both procedures were verified empirically on v1.2.53 with two regular user accounts.

Root cause

TaskFinderModel::getUserQuery() (app/Model/TaskFinderModel.php:60-70) filters tasks only by:

  • owner_id = current user OR id IN (subtasks assigned to current user)
  • task/project status and hide_in_dashboard

There is no project-membership constraint. getOverdueTasksByUser() (TaskFinderModel.php:235-238) has the same shape (eq owner_id only).

ProjectUserRoleModel::removeUser() (app/Model/ProjectUserRoleModel.php:226-229) is a bare delete of the membership row: it neither unassigns tasks nor fires an event, so the owner_id linkage survives revocation.

Contrast: the web dashboard overview (DashboardPagination::getOverview) iterates only over getActiveProjectsByUser($userId), i.e. it applies membership scoping; resource-level API procedures (getTask etc.) re-check membership at call time and correctly return 403 after revocation.

Steps to reproduce

Two regular users, owner (creator of a private project) and victim (no other relation to it):

  1. As owner: createMyPrivateProject with name "secret-project" -> project_id 1 (owner becomes project-manager).
  2. As owner: addProjectUser(project_id=1, user_id=<victim_id>, role="project-member").
  3. As owner: createTask(project_id=1, title="secret task", owner_id=<victim_id>, date_due=<yesterday as Y-m-d>) -> task_id 1.
  4. As victim: getMyDashboard -> the task is listed (legitimate, victim is still a member). Same for getMyOverdueTasks.
  5. As owner: removeProjectUser(project_id=1, user_id=<victim_id>).
  6. As victim: getTask(task_id=1) -> JSON-RPC error 403 "Forbidden" (membership check works).
  7. As victim: getMyDashboard and getMyOverdueTasks -> the task is STILL returned with all fields, including project_name of the private project.

Example request for step 7:

POST /jsonrpc.php
Authorization: Basic <victim credentials>

{"jsonrpc":"2.0","method":"getMyDashboard","id":1}

Impact

Broken Access Control / information disclosure (CWE-863). Any user who was ever assigned a task (or subtask) of a project keeps permanent read access to those task records after losing membership. Task descriptions may contain confidential project content. Exploitation requires a prior assignment relationship; the leak is read-only. CVSS:3.1 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N (6.5, Medium).

Suggested fix

Add a membership constraint to getUserQuery() (and getOverdueTasksByUser()), e.g. project_id IN (SELECT project_id FROM project_has_users WHERE user_id = ...) via projectPermissionModel->getProjectIds(), mirroring the scoping already used by the web dashboard overview.