Skip to main content

praisonai-platform CVE-2026-47408

MEDIUM
Authorization Bypass Through User-Controlled Key (CWE-639)
2026-05-29 https://github.com/MervinPraison/PraisonAI GHSA-27p4-pjqv-whgj
6.5
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
6.5 MEDIUM
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

Lifecycle Timeline

2
Source Code Evidence Fetched
May 29, 2026 - 23:24 vuln.today
Analysis Generated
May 29, 2026 - 23:24 vuln.today

DescriptionGitHub Advisory

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

  1. Attacker is a member of workspace W_attacker and harvests a target issue UUID I_T from any side channel. State: attacker holds I_T.
  2. Attacker sends GET /workspaces/W_attacker/issues/I_T/activity?limit=200 with Authorization: Bearer <attacker_jwt>. State: control flow enters list_issue_activity.
  3. require_workspace_member(W_attacker, attacker) passes. ActivityService.list_for_issue(I_T) runs SELECT * 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.
  4. 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 details JSON 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.
  5. 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).

AnalysisAI

{workspace_id}/issues/{issue_id}/activity endpoint validates workspace membership but passes issue_id directly to an unscoped database query, returning actor identities, action types, and before/after field values from the details JSON blob for any issue UUID reachable by the attacker. No public exploit has been identified at time of analysis, but the vulnerability is source-inspection-verified via the GHSA-27p4-pjqv-whgj advisory.

Technical ContextAI

The affected component is the praisonai-platform Python package (CPE: pkg:pip/praisonai-platform), a multi-tenant project management API built with FastAPI and an async SQLAlchemy session layer. The root cause is CWE-639 (Authorization Through User-Controlled Key): the route handler list_issue_activity at activity.py lines 32-43 extracts workspace_id from the URL path and uses it only for the require_workspace_member dependency gate, then hands the URL-supplied issue_id directly to ActivityService.list_for_issue(issue_id), which executes SELECT * FROM activity WHERE issue_id = :issue_id with no workspace join or filter. The sibling endpoint list_workspace_activity (lines 19-29) is correctly implemented, passing workspace_id into svc.list_for_workspace(workspace_id). The asymmetry between the two handlers is the definitive indicator of the bug. Activity log entries expose actor identity, action type, entity references, and a free-form details JSON blob containing pre- and post-change values for every tracked field, making this a high-fidelity reconnaissance surface.

RemediationAI

Upgrade praisonai-platform to version 0.1.4, which is the vendor-released patched version per GHSA-27p4-pjqv-whgj (https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-27p4-pjqv-whgj). The fix adds a workspace-scoped issue lookup before dispatching to ActivityService: IssueService.get(workspace_id, issue_id) is called first, and a 404 is returned if the issue does not belong to the requesting workspace. If immediate upgrade is not possible, a compensating control is to restrict access to the GET /workspaces/{workspace_id}/issues/{issue_id}/activity endpoint at the API gateway or reverse proxy layer to only allow requests where the workspace_id in the URL matches the authenticated user's own workspace claim - this requires extracting the workspace from the JWT and comparing it to the path parameter, which is non-trivial and error-prone, making patch application strongly preferred. Disabling the entire activity endpoint as a workaround eliminates audit log visibility for all users and is not recommended unless the deployment is externally exposed with no upgrade path.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-47408 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy