A missing hierarchy check on the role-update endpoint let an Admin strip the workspace Owner’s role — seizing effective control of the workspace — just by swapping a user_uuid.
A privilege-escalation flaw was identified in the workspace member role-management API. A user holding Admin privileges could demote the workspace Owner — the highest-privileged role — to a lower tier such as approver, simply by supplying the Owner's user_uuid in a role-update request.
The root cause is a missing server-side authorization check on PATCH /workspace/member. The backend confirmed the requester was an authenticated Admin but never verified whether that Admin was permitted to modify a user of equal or higher privilege. The target is selected entirely from a client-supplied user_uuid, trusted without validating the target's current role against the requester's authority.
Because the Owner controls workspace governance, users, and settings, an Admin who can strip the Owner's role can seize effective control of the workspace, disrupt governance, and potentially lock the legitimate Owner out. This breaks the core invariant of the role model — lower roles must not act on higher roles — and is rated High.
Workspaces use a role hierarchy: Owner (full control of users, roles, settings, ownership), Admin (broad management of members and resources — but not authority over the Owner), and Approver / lower roles (scoped permissions).
The security contract is that an Admin may manage roles at or below their own level. Demoting the Owner lets a subordinate role reach up the hierarchy and neutralize the account above it. Ownership-level changes should be reserved to the Owner via a deliberate, confirmed ownership-transfer flow — never available as a side effect of the general member-role endpoint.
The PATCH /workspace/member endpoint accepts a role, a user_uuid, and a type, and applies the role change to the referenced user. When an Admin calls it:
user_uuid and role. ✅approver. ❌user_uuid with no entitlement check binding the action to the requester's permissions.Documented for defensive validation. Tokens and UUIDs are placeholders/redacted.
Step 1 — Authenticate as Admin and capture the bearer token and workspace context. Step 2 — Capture a role-update request for a user the Admin is allowed to manage, via an HTTP proxy. Step 3 — Retarget the Owner using the Admin's own valid token:
PATCH /workspace/member HTTP/2
Host: api.nected.ai
Authorization: Bearer <ADMIN_AUTH_TOKEN>
Nected-Ws: <WORKSPACE_ID>
Content-Type: application/json
{
"role": "approver",
"user_uuid": "<OWNER_USER_UUID>",
"type": "team"
}
Step 4 — Send. Forward the request. Step 5 — Observe. The server accepts it and demotes the Owner to approver, confirming the backend does not validate whether the Admin is authorized to modify an Owner-level account.
user_uuid trusted blindly (CWE-639). The target is selected from client input with no accompanying entitlement check.Underlying all three is the recurring mistake: authentication was enforced, authorization on the specific target was not.
approver), stripping their control.CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L → ~7.6 (High)
| Metric | Value | Reasoning |
|---|---|---|
| Attack Vector | Network | Exploited over the API. |
| Attack Complexity | Low | Single modified request. |
| Privileges Required | Low | Requires a valid Admin account. |
| User Interaction | None | No victim action needed. |
| Integrity | High | Unauthorized modification of a high-privilege account's role. |
| Availability | Low | Owner loses ability to manage the workspace. |
Scoring note: the vector computes to ~7.6 (High); dropping confidentiality to C:N gives ~7.1, and scoring the escalation as a scope change (S:C) rises to ~8.5. All defensible readings land in the High band.
function updateMemberRole(requester, targetUuid, newRole):
target = lookupMember(requester.workspaceId, targetUuid)
if target is null: return notFound()
# 1. Same-workspace / membership check
if target.workspaceId != requester.workspaceId: return forbidden()
# 2. Owner protection: Owner role may not be changed via this endpoint
if target.role == OWNER:
return forbidden("Use the ownership-transfer flow")
# 3. Hierarchy check: requester must outrank the target and the new role
if roleRank(requester) <= roleRank(target) or roleRank(requester) <= roleRank(newRole):
return forbidden("Insufficient privilege for this role change")
applyRole(target, newRole)
auditLog(actor=requester, action="roleChange", target=targetUuid, newRole)
return success()
user_uuid for sensitive actions without an entitlement check.Broken access control is the #1 web risk — and hierarchy bugs slip past scanners. Farchase tests every role change against real permissions.