← Back to list

How GraphQL Mutation Aliasing Led to a $12,500 DoS Bug in HackerOne’s Account Recovery Flow

A small GraphQL behavior created a very real availability problem.

Abhishek meena in InfoSec Write-ups · 2026-06-08 15:59 · 26 claps · 7.3 min read paywalled
#bug-bounty #infosec #graphql #bug-bounty-tips #bug-bounty-writeup
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity

How GraphQL Mutation Aliasing Led to a $12,500 DoS Bug in HackerOne’s Account Recovery Flow

A small GraphQL behavior created a very real availability problem.

Most bug bounty hunters look for obvious impact.

Account takeover. Sensitive data leaks. IDORs. SSRF. RCE.

But sometimes the bug is not about stealing data.

Sometimes the right question is:

Can the application be forced to do expensive work repeatedly from a single request?

That was the core idea behind this HackerOne report.

In this article, I am breaking down a Denial-of-Service report involving HackerOne’s GraphQL API, where the verifyAccountRecoveryPhoneNumber mutation could be executed multiple times inside one request using GraphQL aliases.

The original report was submitted by @hellokbit. My goal here is to analyze the thinking approach behind the report and make the vulnerability easy to understand for people learning bug bounty and GraphQL security.

The reported bounty was:

$12,500

This writeup breaks down the reporter’s approach in a simple way, especially for beginner to intermediate bug bounty hunters who are learning GraphQL testing.

The Short Version

HackerOne had a GraphQL mutation called:

verifyAccountRecoveryPhoneNumber

This mutation was used during the account recovery phone number verification flow.

The issue was that the same mutation could be repeated multiple times in one GraphQL request using aliases:

verify1: verifyAccountRecoveryPhoneNumber(...)
verify2: verifyAccountRecoveryPhoneNumber(...)
verify3: verifyAccountRecoveryPhoneNumber(...)

Each alias caused the backend to process the mutation separately.

The important observation:

Every additional alias added around 8 seconds of processing time.

So one request could become very expensive.

→ A request with 1 alias took around 8 seconds.

→ A request with 2 aliases took around 16 seconds.

→ A request with 3 aliases could reach 24+ seconds.

More aliases caused timeouts and unstable server behavior.

This turned a normal authenticated GraphQL mutation into a resource exhaustion issue.

What Is GraphQL Aliasing?

If you are new to GraphQL, aliases are a normal feature.

They allow a client to call the same field or mutation multiple times and give each result a different name.

Example:

query {
  user1: user(id: "1") {
    name
  }
  user2: user(id: "2") {
    name
  }
}

Here, user1 and user2 are aliases.

This is useful when you want to fetch similar data in one request.

But with mutations, aliases can become dangerous if the backend does not control execution cost.

Because this:

verify1: expensiveMutation(...)
verify2: expensiveMutation(...)
verify3: expensiveMutation(...)

may not behave like “one request.”

It may behave like:

Run this expensive operation three separate times.

That is exactly what happened here.

Where The Reporter Found It

The affected area was the account recovery phone number setup flow.

The normal user flow was:

  1. Log in as a normal user.
  2. Go to account recovery settings.
  3. Add a phone number.
  4. Enter the verification code.
  5. Intercept the GraphQL request.

The intercepted request called:

verifyAccountRecoveryPhoneNumber

At first, the reporter tested the normal request.

It took around 8 seconds.

That already looked interesting.

Then came the important testing question:

What happens if this mutation is repeated using GraphQL aliases?

The First Proof of Concept

A simplified version of the mutation looked like this:

mutation VerifyAccountRecoveryPhoneNumberMutations(
  $verification_code: String!,
  $otp_code: String
) {
  verify1: verifyAccountRecoveryPhoneNumber(
    input: {
      verification_code: $verification_code,
      otp_code: $otp_code
    }
  ) {
    __typename
    me {
      name
    }
  }
}

With one alias, the response took around 8 seconds.

Then the reporter added a second alias:

mutation VerifyAccountRecoveryPhoneNumberMutations(
  $verification_code: String!,
  $otp_code: String
) {
  verify1: verifyAccountRecoveryPhoneNumber(
    input: {
      verification_code: $verification_code,
      otp_code: $otp_code
    }
  ) {
    __typename
    me {
      name
    }
  }
  verify2: verifyAccountRecoveryPhoneNumber(
    input: {
      verification_code: $verification_code,
      otp_code: $otp_code
    }
  ) {
    __typename
  }
}

Now the response time roughly doubled.

That was the key signal.

The server was not treating this as one logical verification attempt.

It was processing each alias separately.

Why This Matters

At first glance, this may look like self-DoS.

One user sends a slow request. That same user waits longer. So what?

That is a common misunderstanding with DoS bugs.

The actual impact was not “the attacker’s browser waits longer.”

The real issue was server-side resource consumption.

A single authenticated user could send a GraphQL request with multiple aliases and force the backend to perform the same expensive verification operation repeatedly.

Even worse:

The mutation still consumed processing time even with invalid or dummy input.

That means the attacker did not need a valid verification code to trigger the expensive behavior.

They only needed an authenticated account.

The Impact Explanation

When the triager asked for more details, the important points were:

  1. A single authenticated account could trigger the behavior.
  2. Each alias added around 8 to 9 seconds of backend processing.
  3. Two or three aliases could push execution close to timeout.
  4. The server still attempted to process each alias.
  5. Repeated requests could consume server resources and affect availability.

A timeout response was observed like this:

{
  "errors": [
    {
      "message": "Timeout on DynamicFields.__typename",
      "path": ["verify31", "__typename"]
    }
  ],
  "data": null
}

This showed that the server was still processing deep into the aliased mutation list before timing out.

Timeouts help reduce the blast radius, but they do not fully solve the issue.

If each request holds resources for 20 to 30 seconds, an attacker can stack requests and maintain pressure on the application.

Concurrent Request Testing

To demonstrate that this was not just a slow single request, the reporter tested a small number of concurrent requests safely.

The reporter sent 3 concurrent GraphQL requests.

Each request had 4 aliases.

The observed behavior was inconsistent, but that inconsistency was also useful evidence.

The responses included:

1x 500 error after ~29 seconds
1x timeout response after ~26 seconds
1x 500 error after ~17 seconds

This showed unstable behavior under a very small amount of pressure.

No massive traffic was needed.

No large botnet. No high bandwidth. No thousands of requests.

Just a few expensive GraphQL requests.

That is what made the report interesting.

Why GraphQL DoS Bugs Are Different

Traditional DoS often depends on request volume.

GraphQL DoS can be different.

With GraphQL, one request can contain a lot of work.

That work can come from:

  1. Deeply nested queries.
  2. Repeated aliases.
  3. Expensive resolvers.
  4. Mutations that trigger backend workflows.
  5. Missing query complexity limits.
  6. Missing rate limits per operation type.

In this case, the issue was mutation aliasing.

The request looked small.

But the backend work was not small.

That is the lesson.

In GraphQL, request size and backend cost are not always the same thing.

Why This Was Security Relevant

The affected mutation was part of account recovery phone number verification.

Availability matters a lot in recovery flows.

If attackers can degrade recovery-related endpoints, legitimate users may have trouble securing or recovering their accounts.

The potential impact included:

  1. Increased latency for legitimate users.
  2. Timeouts during account recovery setup.
  3. Resource exhaustion on backend workers.
  4. Wider application slowdown if infrastructure resources were shared.
  5. Service instability caused by low-volume expensive requests.

This was an authenticated issue, but that does not remove the impact.

In many bounty programs, authenticated DoS is still valid if a normal user can trigger meaningful infrastructure cost.

The key is proving that it affects more than the attacker’s own session.

Root Cause

The likely root cause was simple:

The API allowed multiple aliases of the same expensive mutation in one GraphQL request without enforcing a strict cost, alias, or mutation execution limit.

The backend executed each alias independently.

So this:

verify1: verifyAccountRecoveryPhoneNumber(...)
verify2: verifyAccountRecoveryPhoneNumber(...)
verify3: verifyAccountRecoveryPhoneNumber(...)

effectively became:

Run verification logic
Run verification logic again
Run verification logic again

If each execution takes around 8 seconds, the math becomes dangerous quickly.

How This Could Be Fixed

There are several practical mitigations for this class of issue.

1. Limit Aliases

Set a maximum number of aliases allowed per request.

This is especially important for mutations.

2. Block Duplicate Expensive Mutations

If the same mutation is repeated multiple times with aliases, reject the request or execute it only once.

3. Add Query Complexity Scoring

GraphQL APIs should calculate cost before execution.

Expensive mutations should have higher cost.

Requests above the allowed cost should be rejected early.

4. Add Resolver-Level Rate Limits

Rate limits should not only be endpoint-based.

For GraphQL, /graphql is usually one endpoint.

So rate limiting should also happen at the operation or resolver level.

5. Add Timeouts and Cancellation

Timeouts are useful, but the backend should also cancel ongoing work cleanly.

Otherwise, timed-out requests may still consume resources.

6. Make Invalid Input Cheap

Invalid verification codes or invalid states should fail quickly.

If dummy input still triggers expensive operations, attackers can abuse that path easily.

What This Report Teaches

This bug was a good reminder that not every high-impact bug looks dramatic.

There was no account takeover. No private data leak. No fancy payload.

The bug came from understanding how GraphQL executes work.

The important testing mindset was:

Don’t only ask, “Can I access something?” Also ask, “Can the server be forced to work harder than it should?”

That mindset is useful in API security.

Especially with GraphQL.

Safe Testing Note

DoS testing is sensitive.

Always follow the program rules.

Only test within allowed limits. Avoid high-volume traffic. Avoid production disruption. Use the smallest proof needed to demonstrate impact. Ask the program if you are unsure.

Also, HackerOne has updated its DoS guidelines as of October 2025 with stricter safety and testing requirements.

This report was submitted and resolved under the previous program guidelines.

So if you are testing similar issues today, review the latest program policy first.

Beginner Takeaways

If you are learning bug bounty, here is what you can take from this report:

  1. GraphQL aliases are normal, but they can create security issues.
  2. Mutations are often more sensitive than queries because they trigger backend actions.
  3. One GraphQL request can cause many backend operations.
  4. Response time is a useful signal when testing expensive functionality.
  5. Invalid input should not trigger expensive processing.
  6. DoS impact must be explained in terms of infrastructure and affected users.
  7. Safe testing matters more than aggressive testing.

Final Thoughts

This bug looked small at first.

Just one mutation. Just one account recovery flow. Just one GraphQL feature.

But the behavior created a real availability risk.

That is why GraphQL security is interesting.

The dangerous part is often not the syntax.

It is the hidden backend cost behind the syntax.

The biggest lesson from this report is simple:

If an API lets users control how much work the server performs, that control needs strict limits.

Security is not only about protecting data.

Security is also product trust.

And availability is part of that trust.


메타데이터
post_id
a0635b2f3997
slug
how-graphql-mutation-aliasing-led-to-a-12-500-dos-bug-in-hackerones-account-recovery-flow-a0635b2f3997
url
https://infosecwriteups.com/how-graphql-mutation-aliasing-led-to-a-12-500-dos-bug-in-hackerones-account-recovery-flow-a0635b2f3997
canonical_url
https://infosecwriteups.com/how-graphql-mutation-aliasing-led-to-a-12-500-dos-bug-in-hackerones-account-recovery-flow-a0635b2f3997
author_url
https://medium.com/@Aacle
status
ok
fetched_at
2026-06-10 09:45:17