Every CVE Search/ Get API key

CVE-2026-47418

praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR

8.1 HighFix available

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 project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. 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
High
Availability
None

Exploit likelihood

0.41%Up 0.4 points since Jun 12, 2026
90%50%10%1%

What each source reports

CVE List Score used 8.1

praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR

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 project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. PraisonAI Platform version 0.1.4 patches the issue.

Record
CVE-2026-47418
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-639
Updated
2026-11-02
EUVDEUVD-2026-46337 8.1

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 project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. PraisonAI Platform version 0.1.4 patches the issue.

Record
EUVD-2026-46337
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-943m-6wx2-rc2j 8.1

praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR

Summary

Type: Insecure Direct Object Reference. The project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. File: src/praisonai-platform/praisonai_platform/services/project_service.py, lines 47-108; route handlers at src/praisonai-platform/praisonai_platform/api/routes/projects.py, lines 51-108. Root cause: identical to the agent and issue IDORs in this codebase. The route accepts workspace_id from URL, uses it solely for the membership gate, then calls ProjectService.get(project_id) which is session.get(Project, project_id) — a primary-key-only lookup with no workspace_id predicate. update and delete call self.get(project_id) first, inheriting the gap. get_stats likewise has no workspace check.

Affected Code

File 1: src/praisonai-platform/praisonai_platform/services/project_service.py, lines 47-108.

class ProjectService:
    ...

    async def get(self, project_id: str) -> Optional[Project]:
        """Get project by ID."""
        return await self._session.get(Project, project_id)         # <-- BUG: no workspace_id predicate

    async def update(
        self,
        project_id: str,
        ...
    ) -> Optional[Project]:
        project = await self.get(project_id)                        # <-- inherits the gap
        ...

    async def delete(self, project_id: str) -> bool:
        project = await self.get(project_id)                        # <-- inherits the gap
        ...

    async def get_stats(self, project_id: str) -> dict:
        ...                                                          # <-- also no workspace check; returns issue counts for any project

File 2: src/praisonai-platform/praisonai_platform/api/routes/projects.py, lines 51-108.

@router.get("/{project_id}", response_model=ProjectResponse)
async def get_project(
    workspace_id: str,
    project_id: str,
    user: AuthIdentity = Depends(require_workspace_member),
    session: AsyncSession = Depends(get_db),
):
    svc = ProjectService(session)
    project = await svc.get(project_id)                             # <-- workspace_id never threaded through
    if project is None:
        raise HTTPException(status_code=404, detail="Project not found")
    return ProjectResponse.model_validate(project)


@router.patch("/{project_id}", response_model=ProjectResponse)
async def update_project(...):
    svc = ProjectService(session)
    project = await svc.update(project_id, title=body.title, ...)   # <-- writes to any project in the DB

@router.delete("/{project_id}", ...)
async def delete_project(...):
    deleted = await svc.delete(project_id)                          # <-- deletes any project in the DB

@router.get("/{project_id}/stats")
async def project_stats(...):
    return await svc.get_stats(project_id)                          # <-- returns stats for any project in the DB

Why it's wrong: workspace_id from the route is treated as a UI hint (gates "are you in some workspace W?") rather than an authoritative predicate (should also gate "is the project you are addressing actually inside W?"). The MemberService in this same codebase uses a composite (workspace_id, user_id) key and demonstrates the safe pattern; the project service simply did not apply it.

Exploit Chain

  1. Attacker registers a workspace W_attacker (where they are a member) and harvests a target project UUID P_T. Project IDs leak through the activity feed (act_svc.log records entity_id),…
Record
GHSA-943m-6wx2-rc2j
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-639
Updated
2026-11-02
NVD 8.1

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 project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. PraisonAI Platform version 0.1.4 patches the issue.

Record
CVE-2026-47418
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-639
Updated
2026-11-02
OSV 8.1

praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR

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 project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. PraisonAI Platform version 0.1.4 patches the issue.

Record
CVE-2026-47418
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-639
Updated
2026-11-02
OSVGHSA-943m-6wx2-rc2j 8.1

praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR

Summary

Type: Insecure Direct Object Reference. The project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. File: src/praisonai-platform/praisonai_platform/services/project_service.py, lines 47-108; route handlers at src/praisonai-platform/praisonai_platform/api/routes/projects.py, lines 51-108. Root cause: identical to the agent and issue IDORs in this codebase. The route accepts workspace_id from URL, uses it solely for the membership gate, then calls ProjectService.get(project_id) which is session.get(Project, project_id) — a primary-key-only lookup with no workspace_id predicate. update and delete call self.get(project_id) first, inheriting the gap. get_stats likewise has no workspace check.

Affected Code

File 1: src/praisonai-platform/praisonai_platform/services/project_service.py, lines 47-108.

class ProjectService:
    ...

    async def get(self, project_id: str) -> Optional[Project]:
        """Get project by ID."""
        return await self._session.get(Project, project_id)         # <-- BUG: no workspace_id predicate

    async def update(
        self,
        project_id: str,
        ...
    ) -> Optional[Project]:
        project = await self.get(project_id)                        # <-- inherits the gap
        ...

    async def delete(self, project_id: str) -> bool:
        project = await self.get(project_id)                        # <-- inherits the gap
        ...

    async def get_stats(self, project_id: str) -> dict:
        ...                                                          # <-- also no workspace check; returns issue counts for any project

File 2: src/praisonai-platform/praisonai_platform/api/routes/projects.py, lines 51-108.

@router.get("/{project_id}", response_model=ProjectResponse)
async def get_project(
    workspace_id: str,
    project_id: str,
    user: AuthIdentity = Depends(require_workspace_member),
    session: AsyncSession = Depends(get_db),
):
    svc = ProjectService(session)
    project = await svc.get(project_id)                             # <-- workspace_id never threaded through
    if project is None:
        raise HTTPException(status_code=404, detail="Project not found")
    return ProjectResponse.model_validate(project)


@router.patch("/{project_id}", response_model=ProjectResponse)
async def update_project(...):
    svc = ProjectService(session)
    project = await svc.update(project_id, title=body.title, ...)   # <-- writes to any project in the DB

@router.delete("/{project_id}", ...)
async def delete_project(...):
    deleted = await svc.delete(project_id)                          # <-- deletes any project in the DB

@router.get("/{project_id}/stats")
async def project_stats(...):
    return await svc.get_stats(project_id)                          # <-- returns stats for any project in the DB

Why it's wrong: workspace_id from the route is treated as a UI hint (gates "are you in some workspace W?") rather than an authoritative predicate (should also gate "is the project you are addressing actually inside W?"). The MemberService in this same codebase uses a composite (workspace_id, user_id) key and demonstrates the safe pattern; the project service simply did not apply it.

Exploit Chain

  1. Attacker registers a workspace W_attacker (where they are a member) and harvests a target project UUID P_T. Project IDs leak through the activity feed (act_svc.log records entity_id),…
Record
GHSA-943m-6wx2-rc2j
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Weakness
CWE-639
Updated
2026-11-02
OSVPYSEC-2026-2932 8.1

praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR

Summary

Type: Insecure Direct Object Reference. The project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. File: src/praisonai-platform/praisonai_platform/services/project_service.py, lines 47-108; route handlers at src/praisonai-platform/praisonai_platform/api/routes/projects.py, lines 51-108. Root cause: identical to the agent and issue IDORs in this codebase. The route accepts workspace_id from URL, uses it solely for the membership gate, then calls ProjectService.get(project_id) which is session.get(Project, project_id) — a primary-key-only lookup with no workspace_id predicate. update and delete call self.get(project_id) first, inheriting the gap. get_stats likewise has no workspace check.

Affected Code

File 1: src/praisonai-platform/praisonai_platform/services/project_service.py, lines 47-108.

class ProjectService:
    ...

    async def get(self, project_id: str) -> Optional[Project]:
        """Get project by ID."""
        return await self._session.get(Project, project_id)         # <-- BUG: no workspace_id predicate

    async def update(
        self,
        project_id: str,
        ...
    ) -> Optional[Project]:
        project = await self.get(project_id)                        # <-- inherits the gap
        ...

    async def delete(self, project_id: str) -> bool:
        project = await self.get(project_id)                        # <-- inherits the gap
        ...

    async def get_stats(self, project_id: str) -> dict:
        ...                                                          # <-- also no workspace check; returns issue counts for any project

File 2: src/praisonai-platform/praisonai_platform/api/routes/projects.py, lines 51-108.

@router.get("/{project_id}", response_model=ProjectResponse)
async def get_project(
    workspace_id: str,
    project_id: str,
    user: AuthIdentity = Depends(require_workspace_member),
    session: AsyncSession = Depends(get_db),
):
    svc = ProjectService(session)
    project = await svc.get(project_id)                             # <-- workspace_id never threaded through
    if project is None:
        raise HTTPException(status_code=404, detail="Project not found")
    return ProjectResponse.model_validate(project)


@router.patch("/{project_id}", response_model=ProjectResponse)
async def update_project(...):
    svc = ProjectService(session)
    project = await svc.update(project_id, title=body.title, ...)   # <-- writes to any project in the DB

@router.delete("/{project_id}", ...)
async def delete_project(...):
    deleted = await svc.delete(project_id)                          # <-- deletes any project in the DB

@router.get("/{project_id}/stats")
async def project_stats(...):
    return await svc.get_stats(project_id)                          # <-- returns stats for any project in the DB

Why it's wrong: workspace_id from the route is treated as a UI hint (gates "are you in some workspace W?") rather than an authoritative predicate (should also gate "is the project you are addressing actually inside W?"). The MemberService in this same codebase uses a composite (workspace_id, user_id) key and demonstrates the safe pattern; the project service simply did not apply it.

Exploit Chain

  1. Attacker registers a workspace W_attacker (where they are a member) and harvests a target project UUID P_T. Project IDs leak through the activity feed (act_svc.log records entity_id),…
Record
PYSEC-2026-2932
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
praisonai-platform
PyPI
before 0.1.4
0.1.4
praisonai-platform
PyPI
before 0.1.4, plus listed versions
0.1.00.1.10.1.20.1.3
0.1.4

History

History
RecordedChangedSources
aliasesEUVD
first indexedCVE List, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-47418 is a high vulnerability (CVSS 8.1) in Mervinpraison Praisonai-platform, published June 1, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.41%, the 35th percentile. Fixed in praisonai-platform 0.1.4.

References

This page doesn't list everything

Left out here: the rest of 3 source descriptions. The JSON record has them.

MoveEnter Open