PT-2026-45062 · Pypi · Praisonai-Platform
Published
2026-05-29
·
Updated
2026-05-29
·
CVE-2026-47408
CVSS v3.1
6.5
Medium
| Vector | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
Summary
Type: Insecure Direct Object Reference. The
GET /workspaces/{workspace id}/issues/{issue id}/activity endpoint is gated by require workspace member(workspace id) and dispatches to ActivityService.list for issue(issue id), which executes SELECT * FROM activity WHERE issue id = :issue id with no workspace constraint. A user who is a member of any workspace can read the full activity log of any issue across the entire multi-tenant deployment.
File: src/praisonai-platform/praisonai platform/api/routes/activity.py, lines 32-43; services/activity service.py's list for issue method.Root cause: the route extracts
workspace id from the URL path, uses it solely for the membership gate, then passes the URL-supplied issue id directly to ActivityService.list for issue(issue id) without verifying which workspace the issue belongs to. The companion list workspace activity endpoint at line 19-29 is implemented correctly (it passes workspace id to svc.list for workspace(workspace id)) — the asymmetry is the smoking gun.Affected Code
File:
src/praisonai-platform/praisonai platform/api/routes/activity.py, lines 19-43.python
@router.get("/activity", response model=List[ActivityLogResponse])
async def list workspace activity(
workspace id: str,
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
user: AuthIdentity = Depends(require workspace member),
session: AsyncSession = Depends(get db),
):
svc = ActivityService(session)
logs = await svc.list for workspace(workspace id, limit=limit, offset=offset) # correct: passes workspace id
return [ActivityLogResponse.model validate(log) for log in logs]
@router.get("/issues/{issue id}/activity", response model=List[ActivityLogResponse])
async def list issue activity(
workspace id: str,
issue id: str,
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
user: AuthIdentity = Depends(require workspace member),
session: AsyncSession = Depends(get db),
):
svc = ActivityService(session)
logs = await svc.list for issue(issue id, limit=limit, offset=offset) # <-- BUG: no workspace id
return [ActivityLogResponse.model validate(log) for log in logs]Why it's wrong: activity logs are typically the most sensitive operational record — they include actor identity, action type, entity references, and a free-form
details JSON blob that may contain pre-/post-change values for any tracked field. Reading the foreign workspace's activity log gives the attacker a high-fidelity view into who did what when, which is gold for further reconnaissance (cross-workspace member enumeration, foreign issue title disclosure, knowing which projects exist). The same author got list workspace activity right by passing workspace id — the issue-scoped variant is the gap.Exploit Chain
- Attacker is a member of workspace
W attackerand harvests a target issue UUIDI Tfrom any side channel. State: attacker holdsI T. - Attacker sends
GET /workspaces/W attacker/issues/I T/activity?limit=200withAuthorization: Bearer <attacker jwt>. State: control flow enterslist issue activity. require workspace member(W attacker, attacker)passes.ActivityService.list for issue(I T)runsSELECT * FROM activity WHERE issue id = 'I T' ORDER BY created at DESC LIMIT 200. State: response body is the full activity log for the foreign issue.- The activity entries reveal: every actor (member or agent) who touched the issue, every action (created, updated, commented, status changed, assignee changed, project changed, label added, dependency added), and the
detailsJSON blob containing the before/after values of every change. State: the attacker fingerprints the foreign workspace's triage workflow, identifies who works on what, and sees the issue's complete history including any embedded secrets that ever passed through the description or comments. - Final state: with one workspace-member token plus one GET, the attacker reads the full activity timeline of any issue in the multi-tenant deployment given the issue UUIDs.
Security Impact
Severity: sec-moderate. CVSS 6.5: network attack, low complexity, low privileges, no user interaction, scope unchanged, high confidentiality (full activity log including before/after
details), no integrity claim (read-only), no availability claim.Attacker capability: read the activity log of any issue in the deployment given its UUID. Combined with the companion issue-IDOR (which already gives full issue content), this is recon for the foreign workspace's operational tempo, member identity, and triage workflow.
Preconditions:
praisonai-platform is deployed multi-tenant; attacker has any workspace-membership token; foreign issue UUIDs are reachable.Differential: source-inspection-verified. The asymmetry between
list workspace activity (correctly workspace-scoped) and list issue activity (no workspace check) confirms the gap. With the suggested fix below, the route first resolves the issue via IssueService.get(workspace id, issue id), returns 404 for foreign issues, and only then proceeds.Suggested Fix
diff
--- a/src/praisonai-platform/praisonai platform/api/routes/activity.py
+++ b/src/praisonai-platform/praisonai platform/api/routes/activity.py
@@ -32,9 +32,12 @@
@router.get("/issues/{issue id}/activity", response model=List[ActivityLogResponse])
async def list issue activity(
workspace id: str,
issue id: str,
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
user: AuthIdentity = Depends(require workspace member),
session: AsyncSession = Depends(get db),
):
+ issue svc = IssueService(session)
+ if await issue svc.get(workspace id, issue id) is None: # workspace-scoped get from issue-IDOR companion
+ raise HTTPException(status code=404, detail="Issue not found")
svc = ActivityService(session)
logs = await svc.list for issue(issue id, limit=limit, offset=offset)
return [ActivityLogResponse.model validate(log) for log in logs]The same single-key issue lookup pattern is filed separately as the IssueService IDOR; once that is fixed, the helper used here is just
IssueService.get(workspace id, issue id).Fix
IDOR
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Praisonai-Platform