Case Study: Admin Can Delete Super Admin — Farchase
Farchase logo Farchase ← All Case Studies Book a Security Call
Case Studies/Privilege Escalation
CRITICAL A01 · Broken Access Control GraphQL API

Admin Can Delete Super Admin

A missing server-side authorization check on a GraphQL removeUser mutation let a lower-privileged Admin delete the organization's highest-privileged account.

CVSS v3.1
8.7 → 9.6
Component
removeUser mutation
Status
Reported · Fix advised
Vulnerability class
Privilege Escalation
CWE
285 · 269 · 639
Endpoint
POST /api/graphql
Root cause
Missing server-side RBAC

Executive Summary

A privilege-escalation flaw was identified in the application's GraphQL API that allowed a user holding only Admin privileges to delete a Super Admin account — the highest-privileged role in the organization.

The root cause was a missing server-side authorization check on the removeUser mutation. The backend accepted a user-supplied target identifier (userEntityId) and executed the deletion without verifying whether the requesting user was permitted to act on a user of equal or higher privilege.

Because the Super Admin typically represents the ultimate owner of an organization's tenant, an attacker with an Admin account could remove that owner, seize effective control of the organization, and potentially lock out legitimate administrators. This is a textbook horizontal-and-vertical access-control failure with organization-wide blast radius — rated Critical.

Background & Context

Multi-tenant SaaS applications commonly implement a role hierarchy:

RoleTypical capabilities
Super Admin / OwnerFull control of the tenant, billing, and all users. Can create/remove Admins.
AdminManage most resources and standard users, but not the owner.
Member / UserDay-to-day usage with limited scope.

The implicit security contract is straightforward: a role can only manage roles at or below its own level. An Admin managing Members is expected; an Admin deleting the Super Admin violates the model's core invariant. The frontend UI almost certainly hides the "remove" action for higher-privileged users — but the API underneath never enforced the same rule, and the API is the real security boundary.

The Vulnerability

What happened

The application exposes a GraphQL mutation, removeUser(userEntityId: String!), that deletes the user matching the supplied identifier. When an Admin invokes it, the backend:

  • Authenticates the session — confirms who the requester is. ✅
  • Executes the deletion against whatever userEntityId was supplied. ✅
  • Never checks whether the requester is authorized to delete that specific, higher-privileged target.

That third step is the failure. Authentication was present; authorization was absent. The system answered "Are you logged in?" but never asked "Are you allowed to do this to this user?"

Why the UI hides this but the API doesn't

This is a classic case of client-side–only access control. The frontend renders a permission-aware interface, but permission logic that lives only in the browser is advisory, not enforceable. Any user can bypass the UI by crafting the underlying API request directly with an intercepting proxy.

Technical Walkthrough

The steps below are documented for defensive validation and remediation verification. The target host and identifiers are placeholders/redacted.

Step 1 — Authenticate as a lower-privileged user. Log in with a standard Admin account and capture its session cookie.

Step 2 — Intercept an authenticated request. Route the browser through an intercepting proxy (Burp Suite, OWASP ZAP) to capture a valid, authenticated GraphQL request and its session token.

Step 3 — Forge the privileged mutation. Replay the request as a removeUser mutation, substituting the Super Admin's entity ID as the target:

POST /api/graphql HTTP/2
Host: <redacted>
Cookie: <ATTACKER_ADMIN_SESSION>
Content-Type: application/json

{
  "operationName": "RemoveUser",
  "variables": {
    "userEntityId": "<SUPER_ADMIN_ID>"
  },
  "query": "mutation RemoveUser($userEntityId: String!) {
    removeUser(userEntityId: $userEntityId)
  }"
}

Step 4 — Send. Submit the request. Step 5 — Observe. The API returns success and the Super Admin account is deleted, despite the requester holding only Admin privileges. The authorization boundary was never enforced.

Root Cause Analysis

The finding decomposes into three distinct backend gaps:

  • No function-level / object-level authorization. The resolver validated the session but not the relationship between requester and target.
  • No privilege-hierarchy validation. There was no comparison of the requester's rank against the target's rank — the rule "you may only remove users at or below your level" was never expressed in code.
  • User-controlled key trusted blindly (CWE-639). The userEntityId came straight from the client and selected the deletion target with no accompanying entitlement check.

Underlying all three is a single architectural mistake: treating the frontend as the enforcement point. The UI hid the action, so the risk was assumed handled. In reality the API is the security boundary — and it was left open.

Impact Assessment

Technical impact

  • Deletion of the highest-privileged account (Super Admin / Owner).
  • Denial of administrative access; potential lock-out of legitimate owners.
  • Collapse of the role-hierarchy security model.

Business impact

  • Organization takeover — removing the owner can leave the attacker as effective top authority, or the tenant orphaned and unmanageable.
  • Availability / continuity — loss of the account controlling billing, provisioning, and recovery can halt administration.
  • Trust & compliance — a broken authorization model undermines tenant-isolation guarantees and may trigger contractual or regulatory exposure.

CVSS v3.1

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:H/A:HBase 8.7 (High)

MetricValueReasoning
Attack VectorNetworkExploited over the API.
Attack ComplexityLowSingle crafted request; no special conditions.
Privileges RequiredHighRequires a valid Admin account.
User InteractionNoneNo victim action needed.
ScopeChangedImpact crosses the intended authorization boundary.
ConfidentialityNoneNo direct data disclosure.
IntegrityHighUnauthorized destruction of a privileged account.
AvailabilityHighLoss of the highest administrative access.

Note on scoring: if the Admin role is treated as low-privilege relative to the Super Admin, PR:L yields 9.6 (Critical). Given the organization-wide blast radius, the finding is triaged as Critical regardless of PR interpretation.

Remediation

Primary fix — enforce authorization server-side

Add an explicit entitlement check inside the removeUser resolver before any deletion occurs:

function removeUser(requester, targetId):
    target = lookupUser(targetId)
    if target is null:
        return notFound()

    # 1. Hierarchy: requester must outrank (or equal) the target
    if roleRank(requester) < roleRank(target):
        return forbidden("Insufficient privilege to remove this user")

    # 2. Protect the owner
    if target.role == SUPER_ADMIN and requester.role != SUPER_ADMIN:
        return forbidden()

    # 3. Tenant boundary
    if requester.orgId != target.orgId:
        return forbidden()

    performDelete(target)
    auditLog(actor=requester, action="removeUser", target=target)
    return success()
  • Never rely on the frontend to enforce permissions — the UI may hide the action; the API must reject it.
  • Validate the role hierarchy on every privileged mutation, not just deletion.
  • Deny by default — require an explicit "allow" decision rather than assuming access.

Defense-in-depth controls

  • Role-hierarchy validation across all sensitive mutations (remove, disable, role-change, transfer-ownership).
  • Audit logging of every critical action (actor, target, timestamp, source IP).
  • Re-authentication / step-up MFA before destructive or high-privilege operations.
  • Soft delete with recovery so an erroneous or malicious deletion is reversible within a retention window.
  • Ownership-transfer guardrails so the last remaining owner cannot be removed.

Lessons Learned

  • Authentication ≠ authorization. Confirming who a user is says nothing about what they may do.
  • The API is the security boundary — not the UI. Any control that lives only in the browser is decoration.
  • Never trust client-supplied identifiers. A user-controlled id must always be paired with an entitlement check.
  • Encode the invariant in code. If "lower roles cannot act on higher roles" isn't an explicit server-side check, it doesn't exist.
  • Test authorization negatively. "Can role X do the thing X should not be able to do?" would have surfaced this immediately.

Summary

Type
Privilege Escalation / Broken Access Control
Endpoint
POST /api/graphql — removeUser
Root cause
Missing server-side RBAC + hierarchy validation
Severity
Critical · CVSS 8.7 → 9.6
Fix
Enforce role-hierarchy authorization server-side; deny by default
Defense-in-depth
Audit logs, step-up MFA, soft delete, ownership guardrails

Does Your API Enforce Authorization?

Most access-control flaws hide behind a permission-aware UI. Farchase tests the boundary attackers actually reach — the API.