GHSA-2fjj-qqg8-fg7x praisonai-platform: Authorization Bypass Through User-Controlled Key
SummaryThe issue create and update endpoints in `praisonai-platform` accept a `project_id` in the request body and persist it without validating that the project belongs to the URL workspace. A user who is a member of workspace `W_B` (and has no access to workspace `W_A`) can create issues that reference a project owned by `W_A`. Because `ProjectService.get_stats()` aggregates issues by `project_id` with no workspace constraint, those foreign issues are then counted in the victim's own legitimate view of their project statistics. This is a cross-tenant integrity violation reachable by an outsider.This is distinct from the path-parameter IDOR family fixed in 0.1.4 (CVE-2026-47415, CVE-2026-47418, CVE-2026-47419). Those fixes scoped object references supplied in the URL path. This report concerns an object reference supplied in the request body at write time, which the 0.1.4 fixes did not cover.Version 0.1.4 fixed a set of path-parameter IDORs by threading `workspace_id` into the service-layer lookups (`get` / `update` / `delete`) and by adding the helpers `ensure_resource_in_workspace()` and `require_issue_in_workspace()` in `api/deps.py`. Those helpers are applied to object references that arrive in the URL path. They are not applied to object references that arrive in the request body on create or update.Details`api/routes/issues.py`, `create_issue` passes the body's `project_id` straight through with no workspace validation:`services/issue_service.py`, `create` persists it as-is:`services/issue_service.py`, `update` has the identical gap on the update path:`services/project_service.py`, `get_stats` aggregates by `project_id` only:Note that the read path is not directly vulnerable. The stats route scopes the project first, so a cross-workspace stats read returns 404:The pollution therefore enters through the write side (issue create/update accepting a foreign `project_id`) and surfaces in the victim's own legitimate read of their project statistics.Proof of conceptTwo unrelated users:Alice, member of workspace `W_A`, owns project `P_A`.Bob, member of workspace `W_B` only. Bob has no access to `W_A` (every direct call to `W_A` resources returns 403).Steps:Alice's project `P_A` has one in-progress issue.
`GET /workspaces/W_A/projects/P_A/stats` returns `{"total": 1, "by_status": {"in_progress": 1}}`.Bob creates issues in his own workspace that reference Alice's project. Repeat 7 times:Each returns 201. Each issue is stored with `workspace_id = W_B` and `project_id = P_A`.Alice reads her own project stats:
`GET /workspaces/W_A/projects/P_A/stats` now returns
`{"total": 8, "by_status": {"done": 7, "in_progress": 1}}`.Bob is not a member of `W_A`, yet data he wrote appears in Alice's project dashboard.ImpactAn unauthorized outsider can inflate or skew the issue counts shown in any workspace's project-statistics view, given only the target `project_id` (a UUID that can be harvested or guessed). The effect is limited to the statistics aggregation; it does not expose the victim's issue contents to the attacker and does not appear in the victim's workspace-scoped issue list. The same unvalidated write path also accepts cross-workspace `parent_issue_id` and `assignee_id` values, which have no aggregation read endpoint today but represent the same dangling cross-workspace reference class and should be fixed together.Suggested fixOn both issue create and update, validate that any body-supplied object reference resolves within the URL workspace before persisting, reusing the existing pattern:Apply the same check to `parent_issue_id` (via `require_issue_in_workspace`) and to `assignee_id`. As defense in depth, scope `get_stats` so it only counts issues whose `workspace_id` matches the project's workspace.
CVSS v3.1 score: 4.3