CVE-2026-47416

CRITICALPre-NVD 9.69.6
EchelonGraph scoreLOW confidence

This critical-severity CVE scores 9.6 under the CNA's CVSS (NVD's own analysis pending). EPSS exploit probability: 0.0%, top 90% of all CVEs by exploit prediction. GitHub Security Advisory data not yet ingested — confidence will rise once GHSA publishes (typical lag: hours to days for open-source ecosystem CVEs; never for infrastructure-only CVEs).

Triggered by: NVD CVSS baseline
Sources: cna:github_m, epss
9.6
EchelonGraph verdictPlan a fixSerious severity, but no confirmed exploitation yet.
  • High severity, but no confirmed exploitation yet
CISA-KEV: Not listedEPSS: 0%CVSS: 9.6Exploit: NoneExposed: 0

No vendor fix yet — apply a workaround or compensating control (WAF / firewall / segmentation) and watch for a patch.

praisonai-platform: Any workspace member can promote themselves or others to owner via PATCH /workspaces/{id}/members/{user_id}

Summary

Type: Vertical privilege escalation. The PATCH /workspaces/{workspace_id}/members/{user_id} endpoint is gated by require_workspace_member(workspace_id), which defaults to min_role="member" and is never overridden by the route. The handler then calls MemberService.update_role(workspace_id, user_id, body.role) which sets the target member's role to whatever the request body specifies, with no check that the caller has owner-or-admin privilege, no check that the new role is not higher than the caller's own, and no check that the caller is not silently promoting themselves. File: src/praisonai-platform/praisonai_platform/api/routes/workspaces.py, lines 115-127; services/member_service.py, lines 55-69; api/deps.py, lines 54-73. Root cause: require_workspace_member exists with a min_role parameter (deps.py:58) but FastAPI's Depends(require_workspace_member) cannot pass arguments, so every route uses the default "member". The route then passes the URL-supplied user_id and the body-supplied role directly to MemberService.update_role, which contains zero permission checks: it loads the member by composite key and assigns member.role = new_role. A user with the lowest possible privilege ("member") thus sets their own role to "owner" with one HTTP PATCH, completing a member-to-owner privilege escalation in a single request.

Affected Code

File 1: src/praisonai-platform/praisonai_platform/api/routes/workspaces.py, lines 115-127.

@router.patch("/{workspace_id}/members/{user_id}", response_model=MemberResponse)
async def update_member_role(
    workspace_id: str,
    user_id: str,
    body: MemberUpdate,
    user: AuthIdentity = Depends(require_workspace_member),         # <-- BUG: defaults to min_role="member"; no role gate
    session: AsyncSession = Depends(get_db),
):
    member_svc = MemberService(session)
    member = await member_svc.update_role(workspace_id, user_id, body.role)  # <-- writes any role to any member
    if member is None:
        raise HTTPException(status_code=404, detail="Member not found")
    return MemberResponse.model_validate(member)

File 2: src/praisonai-platform/praisonai_platform/services/member_service.py, lines 55-69.

async def update_role(
    self,
    workspace_id: str,
    user_id: str,
    new_role: str,
) -> Optional[Member]:
    """Update a member's role."""
    if new_role not in VALID_ROLES:                                  # only validates the *value*, not the *caller's right*
        raise ValueError(f"Invalid role: {new_role}. Must be one of {VALID_ROLES}")
    member = await self.get(workspace_id, user_id)
    if member is None:
        return None
    member.role = new_role                                           # <-- BUG: no caller-role check, no target-vs-caller hierarchy check
    await self._session.flush()
    return member

File 3: src/praisonai-platform/praisonai_platform/api/deps.py, lines 54-73.

async def require_workspace_member(
    workspace_id: str,
    user: AuthIdentity = Depends(get_current_user),
    session: AsyncSession = Depends(get_db),
    min_role: str = "member",                                        # <-- default that no route overrides
) -> AuthIdentity:
    member_svc = MemberService(session)
    has = await member_svc.has_role(workspace_id, user.id, min_role)
    if not has:
        raise HTTPException(status_code=403, detail="Not a member of this workspace or insufficient role")
    user.workspace_id = workspace_id
    return user

Why it's wrong: require_workspace_member was clearly designed to be tunable per-route — the min_role parameter is right there — but Depends(require_workspace_member) in FastAPI cannot pass arguments to a dependency, so every route resolves to the default "member". The author's intent is also evident in MemberService.has_role (member_service.py:80-96), which implements an owner > admin > member hierarchy that this endpoint should be enforcing. The endpoint uses none of it. The VALID_ROLES = {"owner", "admin", "member"} enum check (member_service.py:62) only validates the *new role string is recognised*, not that the *caller has the right to assign it*. As a result, a member can write {"role": "owner"} to their own membership row and become owner in one PATCH.

Exploit Chain

  • Attacker registers an account and joins (or is invited to) any workspace W as a "member" (the lowest privilege tier — typically anyone can be added by an owner during onboarding, or self-joins via an invite link). State: attacker has a JWT, is a Member(workspace_id=W, user_id=attacker, role="member").
  • Attacker sends PATCH /workspaces/W/members/ with Authorization: Bearer and body {"role": "owner"}. State: control flow enters update_member_role.
  • require_workspace_member(W, attacker) runs. Its default min_role="member" is satisfied because the attacker is a member. The dependency returns the attacker's identity. State: route handler proceeds with no further role gate.
  • MemberService.update_role(W, attacker, "owner") runs. VALID_ROLES accepts "owner". self.get(W, attacker) returns the attacker's existing member row. The next line, member.role = "owner", mutates the attacker's role in place. await self._session.flush() commits. State: attacker is now Member(workspace_id=W, user_id=attacker, role="owner").
  • Attacker re-issues GET /auth/me (or any owner-gated endpoint) and is now treated as workspace owner. State: full administrative control of the workspace, including the ability to add/remove members, change settings, delete the workspace, and exfiltrate everything via the agent/issue/project/comment IDORs that were filed as separate advisories.
  • Final state: starting from the lowest workspace privilege, the attacker holds owner of the workspace within one HTTP request. The same primitive also lets the attacker DEMOTE the legitimate owner by sending PATCH /workspaces/W/members/ with {"role": "member"} — owner lockout in two requests total.

Security Impact

Severity: sec-critical. CVSS 9.1: network attack, low complexity, low privileges (the lowest tier on the platform), no user interaction, scope changed (the privilege boundary the attacker crosses is the workspace owner, a different security principal), high confidentiality and integrity (full workspace control), no availability claim (the attacker can also DELETE the workspace via the companion delete_workspace advisory, but that is a separate finding). Attacker capability: with one workspace-member token plus one PATCH request, the attacker becomes workspace owner. From there: add/remove any user as owner, change every workspace setting (including the settings JSON blob), demote the legitimate owner to "member", or chain into the companion delete_workspace advisory to wipe the workspace entirely. In multi-tenant SaaS deployments where any signup yields a member-level account in some default workspace, this is effectively pre-auth. Preconditions: praisonai-platform is deployed multi-tenant (more than one workspace exists OR the deployment grants member access on signup); the attacker has any membership token in the target workspace. Differential: source-inspection-verified end-to-end. The asymmetry between require_workspace_member's min_role parameter (which exists, defaults to "member", and is never overridden) and MemberService.has_role's clearly tiered owner > admin > member hierarchy (which exists but is never invoked with anything but the default) is the smoking gun. With the suggested fix below, the route resolves with min_role="owner", the attacker's member-level token fails the gate at the dependency, and the privilege escalation never reaches the service layer.

Suggested Fix

The fix has two parts. First, the route must resolve require_workspace_member with min_role="owner" (or at least "admin"). Second, MemberService.update_role should refuse to set a target's role higher than the caller's own role, so that an admin cannot accidentally produce another owner.

--- a/src/praisonai-platform/praisonai_platform/api/routes/workspaces.py
+++ b/src/praisonai-platform/praisonai_platform/api/routes/workspaces.py
@@ -115,11 +115,16 @@
+def _require_owner(workspace_id: str, user, session):
+    return require_workspace_member(workspace_id, user, session, min_role="owner")
+
 @router.patch("/{workspace_id}/members/{user_id}", response_model=MemberResponse)
 async def update_member_role(
     workspace_id: str,
     user_id: str,
     body: MemberUpdate,
  • user: AuthIdentity = Depends(require_workspace_member),
+ user: AuthIdentity = Depends(_require_owner), session: AsyncSession = Depends(get_db), ): member_svc = MemberService(session) + if not await member_svc.has_role(workspace_id, user.id, "owner"): + raise HTTPException(status_code=403, detail="Only owners can change member roles") member = await member_svc.update_role(workspace_id, user_id, body.role)

Defence-in-depth in the service layer:

--- a/src/praisonai-platform/praisonai_platform/services/member_service.py
+++ b/src/praisonai-platform/praisonai_platform/services/member_service.py
@@ -55,7 +55,7 @@
  • async def update_role(self, workspace_id: str, user_id: str, new_role: str) -> Optional[Member]:
+ async def update_role(self, workspace_id: str, caller_id: str, user_id: str, new_role: str) -> Optional[Member]: """Update a member's role.""" + if not await self.has_role(workspace_id, caller_id, "owner"): + raise PermissionError("Only owners can update member roles") if new_role not in VALID_ROLES: raise ValueError(...)

The companion endpoints add_member, remove_member, delete_workspace, and update_workspace exhibit the same Depends(require_workspace_member) default-min-role pattern and are filed as their own advisories so each gets a separate CVE.

CVSS v3
9.6
EG Score
9.6(low)
EPSS
9.7%
KEV
Not listed

Published

May 29, 2026

Last Modified

May 29, 2026

Vendor Advisories for CVE-2026-47416(1)

These vendors published their own advisory mentioning this CVE — often with vendor-specific remediation steps + affected product lists not in NVD.

Affected Packages

(1 across 1 ecosystem)
PyPI(1)
PackageVulnerable rangeFixed inDependents
praisonai-platform0.1.0, 0.1.1, 0.1.2, 0.1.30.1.4

Data Freshness Timeline

(refreshed 7× in last 7d / 56× in last 30d)

Each row is a source pipeline that fetched or updated this CVE on that date, with what changed. For example, "NVD update" means NVD published or revised its analysis for this CVE; "MITRE cvelistV5" means we ingested or refreshed it from the CNA feed. Most recent first.

Showing the most recent 100 of 148 total refreshes for this CVE.

  1. 2026-07-16 09:59 UTCEG score recompute
  2. 2026-07-16 01:45 UTCEG score recompute
  3. 2026-07-14 18:29 UTCEG score recompute
  4. 2026-07-14 13:43 UTCEG score recompute
  5. 2026-07-12 01:24 UTCEG score recompute
  6. 2026-07-11 21:09 UTCEG score recompute
  7. 2026-07-11 16:52 UTCEG score recompute
  8. 2026-07-08 22:42 UTCEG score recompute
  9. 2026-07-08 17:13 UTCEG score recompute
  10. 2026-07-02 23:09 UTCEG score recompute
  11. 2026-07-02 18:55 UTCEG score recompute
  12. 2026-07-02 12:48 UTCEG score recompute
  13. 2026-07-02 08:08 UTCEG score recompute
  14. 2026-07-02 03:41 UTCEG score recompute
  15. 2026-07-01 18:49 UTCEG score recompute
  16. 2026-07-01 12:54 UTCEG score recompute
  17. 2026-07-01 08:36 UTCEG score recompute
  18. 2026-07-01 01:16 UTCEG score recompute
  19. 2026-06-30 05:35 UTCEG score recompute
  20. 2026-06-30 01:06 UTCEG score recompute
  21. 2026-06-29 20:53 UTCEG score recompute
  22. 2026-06-29 16:38 UTCEG score recompute
  23. 2026-06-29 12:25 UTCEG score recompute
  24. 2026-06-29 00:18 UTCEG score recompute
  25. 2026-06-28 20:05 UTCEG score recompute
Show 75 more
  1. 2026-06-28 15:52 UTCEG score recompute
  2. 2026-06-28 11:13 UTCEG score recompute
  3. 2026-06-28 07:00 UTCEG score recompute
  4. 2026-06-28 02:46 UTCEG score recompute
  5. 2026-06-27 22:33 UTCEG score recompute
  6. 2026-06-27 18:20 UTCEG score recompute
  7. 2026-06-27 14:07 UTCEG score recompute
  8. 2026-06-27 09:50 UTCEG score recompute
  9. 2026-06-27 05:36 UTCEG score recompute
  10. 2026-06-27 01:23 UTCEG score recompute
  11. 2026-06-26 20:22 UTCEG score recompute
  12. 2026-06-26 16:09 UTCEG score recompute
  13. 2026-06-26 01:48 UTCEG score recompute
  14. 2026-06-25 19:51 UTCEG score recompute
  15. 2026-06-25 15:37 UTCEG score recompute
  16. 2026-06-25 11:24 UTCEG score recompute
  17. 2026-06-25 07:10 UTCEG score recompute
  18. 2026-06-25 02:45 UTCEG score recompute
  19. 2026-06-24 22:31 UTCEG score recompute
  20. 2026-06-24 15:08 UTCEG score recompute
  21. 2026-06-24 02:40 UTCEG score recompute
  22. 2026-06-18 22:19 UTCEG score recompute
  23. 2026-06-18 10:14 UTCEG score recompute
  24. 2026-06-18 02:10 UTCEG score recompute
  25. 2026-06-17 21:56 UTCEG score recompute
  26. 2026-06-17 17:40 UTCEG score recompute
  27. 2026-06-17 12:29 UTCEG score recompute
  28. 2026-06-17 07:56 UTCEG score recompute
  29. 2026-06-17 03:43 UTCEG score recompute
  30. 2026-06-16 23:31 UTCEG score recompute
  31. 2026-06-16 19:18 UTCEG score recompute
  32. 2026-06-16 15:04 UTCEG score recompute
  33. 2026-06-16 09:49 UTCEG score recompute
  34. 2026-06-16 05:37 UTCEG score recompute
  35. 2026-06-16 01:23 UTCEG score recompute
  36. 2026-06-15 21:09 UTCEG score recompute
  37. 2026-06-15 16:30 UTCEG score recompute
  38. 2026-06-15 12:17 UTCEG score recompute
  39. 2026-06-15 07:40 UTCEG score recompute
  40. 2026-06-15 03:06 UTCEG score recompute
  41. 2026-06-14 23:18 UTCEPSS rescore
  42. 2026-06-14 22:52 UTCEG score recompute
  43. 2026-06-14 18:38 UTCEG score recompute
  44. 2026-06-14 14:26 UTCEG score recompute
  45. 2026-06-14 10:12 UTCEG score recompute
  46. 2026-06-14 05:59 UTCEG score recompute
  47. 2026-06-14 01:45 UTCEG score recompute
  48. 2026-06-13 21:32 UTCEG score recompute
  49. 2026-06-13 17:18 UTCEG score recompute
  50. 2026-06-13 12:27 UTCEG score recompute
  51. 2026-06-13 08:04 UTCEG score recompute
  52. 2026-06-13 03:50 UTCEG score recompute
  53. 2026-06-12 23:37 UTCEG score recompute
  54. 2026-06-12 23:12 UTCEPSS rescore
  55. 2026-06-12 19:23 UTCEG score recompute
  56. 2026-06-12 15:09 UTCEG score recompute
  57. 2026-06-12 10:56 UTCEG score recompute
  58. 2026-06-12 06:43 UTCEG score recompute
  59. 2026-06-12 02:30 UTCEG score recompute
  60. 2026-06-11 22:17 UTCEG score recompute
  61. 2026-06-11 18:04 UTCEG score recompute
  62. 2026-06-11 13:51 UTCEG score recompute
  63. 2026-06-11 09:38 UTCEG score recompute
  64. 2026-06-11 05:25 UTCEG score recompute
  65. 2026-06-11 01:12 UTCEG score recompute
  66. 2026-06-10 20:57 UTCEG score recompute
  67. 2026-06-10 16:44 UTCEG score recompute
  68. 2026-06-10 12:31 UTCEG score recompute
  69. 2026-06-10 08:18 UTCEG score recompute
  70. 2026-06-10 04:06 UTCEG score recompute
  71. 2026-06-09 23:53 UTCEG score recompute
  72. 2026-06-09 19:40 UTCEG score recompute
  73. 2026-06-09 15:27 UTCEG score recompute
  74. 2026-06-09 11:14 UTCEG score recompute
  75. 2026-06-09 07:01 UTCEG score recompute

Frequently asked(5)

What is CVE-2026-47416?
CVE-2026-47416 is a critical vulnerability published on May 29, 2026. praisonai-platform: Any workspace member can promote themselves or others to owner via PATCH /workspaces/{id}/members/{user_id} Summary Type: Vertical privilege escalation. The PATCH /workspaces/{workspaceid}/members/{userid} endpoint is gated by requireworkspacemember(workspaceid), which defaults…
When was CVE-2026-47416 disclosed?
CVE-2026-47416 was first published in the National Vulnerability Database on May 29, 2026. EchelonGraph re-ingests CVE updates from NVD on a 2-hour cycle, so this page reflects the latest published state.
Is CVE-2026-47416 actively exploited?
CVE-2026-47416 is not currently on CISA's Known Exploited Vulnerabilities catalog. FIRST EPSS estimates a 9.7% percentile likelihood of exploitation in the next 30 days — higher percentiles indicate greater predicted risk.
What is the CVSS score of CVE-2026-47416?
CVE-2026-47416 has a CVSS v4.0 base score of 9.6 (CNA self-assessment; NVD's own analysis pending). The EG score is currently aggregating — additional source signals are being incorporated as they become available..
How do I remediate CVE-2026-47416?
Patch to the fixed version published by the affected vendor. Where vendor advisories exist for CVE-2026-47416, EchelonGraph cross-links them in the Vendor Advisories panel below — those typically contain the canonical remediation steps, fixed version numbers, and any vendor-specific mitigations.

Dependency Blast Radius

See which npm, PyPI, Go, and Maven packages are affected by CVE-2026-47416

Explore →

Is Your Infrastructure Affected by CVE-2026-47416?

EchelonGraph automatically scans your cloud infrastructure and maps CVE exposure using blast radius analysis.