CVE-2026-47408
praisonai-platform: list_issue_activity returns activity log for any issue regardless of workspace ownership
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.5
praisonai-platform: list_issue_activity returns activity log for any issue regardless of workspace ownership
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47408- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
EUVDEUVD-2026-46322
6.5
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
EUVD-2026-46322- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-27p4-pjqv-whgj
6.5
praisonai-platform: list_issue_activity returns activity log for any issue regardless of workspace ownership
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.
@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…
- Record
GHSA-27p4-pjqv-whgj- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
NVD
6.5
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47408- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSV
6.5
praisonai-platform: list_issue_activity returns activity log for any issue regardless of workspace ownership
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47408- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVGHSA-27p4-pjqv-whgj
6.5
praisonai-platform: list_issue_activity returns activity log for any issue regardless of workspace ownership
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.
@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…
- Record
GHSA-27p4-pjqv-whgj- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVPYSEC-2026-2927
6.5
praisonai-platform: list_issue_activity returns activity log for any issue regardless of workspace ownership
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.
@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…
- Record
PYSEC-2026-2927- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
praisonai-platformPyPI |
through 0.1.2 | 0.1.4 |
praisonai-platformPyPI |
before 0.1.4, plus listed versions 0.1.00.1.10.1.20.1.3 |
0.1.4 |
History
| Recorded | Changed | Sources |
|---|---|---|
| aliases | EUVD | |
| first indexed | CVE List, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-47408 is a medium vulnerability (CVSS 6.5) in Mervinpraison Praisonai-platform, published May 29, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.29%, the 22nd percentile. Fixed in praisonai-platform 0.1.4.
References
github.com/MervinPraison/PraisonAI/security/advisories/GHSA-27p4-pjqv-whgj
github.com/MervinPraison/PraisonAI/pull/1685
github.com/MervinPraison/PraisonAI/commit/ef79b7a0561796ad9807f0f09538c25cc78d3619
github.com/advisories/GHSA-27p4-pjqv-whgj
github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47408.json
nvd.nist.gov/vuln/detail/CVE-2026-47408
github.com/MervinPraison/PraisonAI
pypi.org/project/praisonai-platform
Left out here: the rest of 3 source descriptions. The JSON record has them.