# From Profile IDOR to Zero-Click Account Takeover: Changing One userid Parameter Was Enough

**Severity:** Critical  
**Bounty:** $1,805  
**Program:** Private Bug Bounty  
**Platform:** Bugbounty.sa

This started as a straightforward profile IDOR.

An authenticated user could change a `userid` parameter and load another registered user’s complete profile inside the account update page.

The response exposed personal information, business details, and address information belonging to the selected account.

That alone was serious enough to report.

But the profile was not read-only.

By keeping the target user’s ID in the update request, I could replace their primary email address with one under my control. I then used the application’s normal password-reset functionality, received the reset email, set a new password, and logged into the target account.

A profile IDOR had become a zero-click account takeover.

* * *

## Finding the Profile Request

The application allowed registered users to manage their personal and business information through an **Update User Profile** page.

The normal flow was simple:

1.  Log into an account.
    
2.  Open **My Account**.
    
3.  Select **Update Profile**.
    
4.  Edit the account information.
    
5.  Submit the changes.
    

While intercepting this workflow, I noticed a POST request similar to:

```http
POST /registration/[redacted] HTTP/1.1
Host: www.[redacted].com
Cookie: [REDACTED]
Content-Type: application/x-www-form-urlencoded
```

The request body included several navigation parameters, followed by:

```text
userid=researcher_account&action=update
```

The parameter that stood out was:

```text
userid=researcher_account
```

The application already had an authenticated session cookie.

It should have known which account belonged to the logged-in user.

There was no reason for the client to decide which user profile the server should load.

That immediately raised the usual IDOR question:

> What happens if I replace my own user ID with another valid one?

* * *

## Switching Between Two Accounts

I created two researcher-controlled accounts.

While authenticated as the first account, the request contained:

```text
userid=first_test_account
```

I changed it to:

```text
userid=second_test_account
```

Nothing else in the request needed to change.

The server returned the **Update User Profile** page for the second account.

The page was rendered directly in the response, with the second account’s information already populated inside the form.

I then changed the parameter back to the first account.

The response switched back to the first account’s details.

The behavior was consistent:

```text
Authenticated as Account A
          ↓
userid=Account A
          ↓
Account A profile returned
```

Changing only the parameter produced:

```text
Authenticated as Account A
          ↓
userid=Account B
          ↓
Account B profile returned
```

The server was using the client-supplied username to select the account instead of binding the profile to the authenticated session.

* * *

## What the Response Exposed

The response did not contain a few harmless public profile fields.

It returned the complete profile management page.

The exposed information included:

*   first name
    
*   last name
    
*   primary email address
    
*   phone number
    
*   company name
    
*   company type
    
*   industry
    
*   sub-industry
    
*   street address
    
*   city
    
*   postal code
    
*   country
    
*   state or province
    
*   account user ID
    

A simplified version of the rendered form looked like:

```html
<input name="firstName" value="[REDACTED]">
<input name="lastName" value="[REDACTED]">

<input name="email" value="[REDACTED]">
<input name="confirmEmail" value="[REDACTED]">
<input name="phoneNumber" value="[REDACTED]">

<input name="companyName" value="[REDACTED]">
<select name="companyType">[REDACTED]</select>
<select name="industry">[REDACTED]</select>
<select name="subIndustry">[REDACTED]</select>

<input name="address1" value="[REDACTED]">
<input name="address2" value="[REDACTED]">
<input name="city" value="[REDACTED]">
<input name="postalCode" value="[REDACTED]">

<input name="userId" value="[TARGET USER ID]">
```

The application exposed enough information to build a detailed personal and business profile of any affected user.

* * *

## Initial Impact: Cross-Account PII Exposure

Any authenticated user who knew another valid user ID could retrieve that user’s profile.

The initial attack flow was:

```text
Valid low-privileged account
          ↓
Supply another user's ID
          ↓
Server loads the selected profile
          ↓
Personal, business, and address data exposed
```

The exposed data could support:

*   targeted phishing
    
*   business impersonation
    
*   social engineering
    
*   employee and company mapping
    
*   account-recovery attacks
    
*   combining personal details with information from other breaches
    

But the location of the vulnerability was just as important as the information being returned.

This was not a public profile page.

It was an account update workflow.

* * *

## Why I Continued Testing

A read IDOR inside a state-changing feature should not be treated as an ordinary information disclosure.

The application had already demonstrated that it trusted the `userid` parameter when deciding which profile to load.

The next question was obvious:

> Would it trust the same `userid` when profile changes were submitted?

Many applications reuse the same object-selection logic for both reading and updating data.

The vulnerable logic may have effectively looked like:

```php
$user = findUser($_POST['userid']);
$profile = loadProfile($user);
```

The secure version should have derived the account from the authenticated session:

```php
$user = authenticatedUser();
$profile = loadProfile($user);
```

The required ownership check was simple:

```text
requested user == authenticated user
```

That check was missing from the profile retrieval flow.

I continued testing the update functionality using only the two accounts under my control.

* * *

## Changing the Target Account’s Primary Email

After loading the second test account through the manipulated `userid`, I modified its primary email address.

The request was sent using the first account’s authenticated session while the body continued to reference the second account.

Conceptually, the important parts were:

```http
POST /registration/[redacted] HTTP/1.1
Host: www.[redacted].com
Cookie: [ACCOUNT A SESSION]
Content-Type: application/x-www-form-urlencoded
```

```text
userid=account_b
&email=attacker-controlled@example.com
&confirmEmail=attacker-controlled@example.com
&action=[redacted]
```

The critical mismatch was:

```text
Authenticated session: Account A
Target userid: Account B
New primary email: Attacker-controlled address
```

The server accepted the request.

It did not verify that the account referenced by `userid` belonged to the authenticated session.

The primary email address for Account B was changed to the email address under my control.

At this point, the issue was no longer limited to reading another user’s information.

I could modify an account-sensitive field belonging to another user.

* * *

## Using Password Reset to Complete the Takeover

Once the target account’s primary email had been replaced, I used the application’s normal password-reset feature.

The reset functionality itself behaved as designed.

It sent the password-reset message to the primary email address currently registered on the account.

The problem was that I had just changed that address through the IDOR.

The password-reset email was delivered to my attacker-controlled inbox.

I followed the reset link, selected a new password, and successfully logged into the target account.

The complete attack chain was:

```text
Log into Account A
          ↓
Change userid to Account B
          ↓
Load Account B's editable profile
          ↓
Replace Account B's primary email
          ↓
Request a password reset
          ↓
Reset email delivered to attacker
          ↓
Set a new password
          ↓
Log into Account B
```

This escalated the original profile IDOR into a complete account takeover.

* * *

## Why It Was Zero-Click

The attack required no interaction from the affected user.

The victim did not need to:

*   click a malicious link
    
*   open an attacker-controlled page
    
*   approve the email change
    
*   provide an OTP
    
*   respond to a message
    
*   communicate with the attacker
    

Every step was performed directly through the vulnerable application.

The attacker only needed:

*   a valid low-privileged account
    
*   the target user’s ID
    
*   an email address under their control
    

The application allowed the target account’s primary email address to be changed without properly confirming that the authenticated user owned that account.

Once the email was replaced, the legitimate password-reset flow completed the takeover.

* * *

## The Password-Reset Feature Was Not the Vulnerability

It is important to separate the two parts of the chain.

The password-reset feature correctly sent the reset message to the email address registered on the account.

The vulnerability was the unauthorized email change that happened first.

The chain was:

```text
Broken profile authorization
          ↓
Unauthorized primary email replacement
          ↓
Legitimate password-reset process
          ↓
Account takeover
```

Without the IDOR, the reset functionality would not have been useful to the attacker.

Without the password-reset functionality, the unauthorized email change would still have been a serious account-integrity issue.

Together, they produced a reliable takeover path.

* * *

## What Actually Failed

The root cause was not simply that profile information was visible.

The deeper problem was that the application treated a client-supplied username as trusted authorization context.

The backend effectively behaved like:

```php
$targetUser = findUser($_POST['userid']);

loadOrUpdateProfile(
    $targetUser,
    $_POST
);
```

Instead of selecting the account from the authenticated session:

```php
$targetUser = authenticatedUser();

loadOrUpdateProfile(
    $targetUser,
    $_POST
);
```

Where cross-account administration is required, the application should perform an explicit authorization check:

```php
if (
    $targetUser->id !== authenticatedUser()->id
    && !authenticatedUser()->canManage($targetUser)
) {
    abort(403);
}
```

The application verified that the requester was logged in.

It did not verify that the requester was authorized to access or modify the account referenced by `userid`.

That distinction is the core of an IDOR:

```text
Authentication:
"Who are you?"

Authorization:
"Are you allowed to access this specific account?"
```

The application answered the first question.

It failed to answer the second.

* * *

## Demonstrated Impact

The vulnerability allowed an authenticated attacker to:

1.  Load another user’s complete profile.
    
2.  Access their personal information.
    
3.  Access their business information.
    
4.  Access their address information.
    
5.  Modify their primary email address.
    
6.  Redirect password-recovery messages to an attacker-controlled inbox.
    
7.  Set a new password.
    
8.  Log into the affected account.
    

The final impact was:

```text
Cross-account PII exposure
          +
Unauthorized profile modification
          +
Primary email reassignment
          +
Password-reset redirection
          =
Zero-click account takeover
```

This justified the Critical severity.

* * *

## Reporting and Triage

I initially submitted the issue as an IDOR exposing full personal and business profile information.

The report included:

*   the vulnerable request
    
*   the affected `userid` parameter
    
*   reproduction steps using controlled accounts
    
*   screenshots showing two different profiles
    
*   a video proof of concept
    
*   remediation recommendations
    

During the initial review, the triager reported that the main website was not loading.

The affected application was still accessible through its direct route, so I provided the working path.

While the report was under review, I completed the deeper testing and submitted a second video showing the zero-click account takeover.

The triager confirmed the vulnerability and informed me that it qualified for a bounty.

The report was rated Critical and awarded **$1,805**.

* * *

## The Fix

The company introduced OTP verification for profile updates.

After the remediation, sensitive profile changes could no longer be completed using only an authenticated session and a manipulated user reference.

The updated flow became:

```text
Request sensitive profile change
          ↓
OTP verification required
          ↓
Account ownership confirmed
          ↓
Change accepted
```

I repeated the original attack path and confirmed that the unauthorized email change and takeover chain no longer worked.

The report then moved through:

```text
Approved
   ↓
Confirmed
   ↓
Resolved
```

* * *

## Hunter Takeaways

### A Read IDOR May Only Be the Beginning

The first visible impact was another user’s profile information.

It would have been easy to stop there.

But the vulnerable request belonged to an update workflow, which made unauthorized modification the natural next thing to test.

The difference was significant:

```text
Cross-account PII exposure
```

became:

```text
Zero-click account takeover
```

When an IDOR appears inside a state-changing area, follow the complete workflow.

* * *

### Usernames Are Object References Too

IDOR testing often focuses on numeric parameters:

```text
user_id=1024
order_id=5512
invoice_id=8911
```

The object reference in this case was a username:

```text
userid=target_username
```

An identifier does not need to be numeric or sequential.

It only needs to identify a resource that the server fails to authorize correctly.

Potential object references include:

*   usernames
    
*   email addresses
    
*   phone numbers
    
*   UUIDs
    
*   account numbers
    
*   customer references
    
*   registration numbers
    

Do not ignore a parameter simply because it contains a readable string instead of an integer.

* * *

### Render HTML Responses

The raw response contained the profile data, but rendering the page made the authorization failure much easier to understand.

Changing one parameter visibly switched:

*   the personal details
    
*   the business details
    
*   the address details
    
*   the displayed account ID
    

For reports involving server-rendered HTML, a comparison between two controlled accounts can communicate the issue more clearly than several pages of response source.

* * *

### Test Reads and Writes Separately

A read IDOR does not automatically prove that an attacker can modify the object.

The application may enforce different controls when loading and saving data.

Each operation should be tested independently.

In this case, controlled testing confirmed that the broken account reference extended from profile retrieval into profile modification.

That evidence was what supported the account-takeover claim.

* * *

### Look for Recovery-Flow Chains

An unauthorized email change is often more dangerous than it first appears.

The next questions should be:

*   Where are password-reset messages sent?
    
*   Does changing the email require the current password?
    
*   Is the old email notified?
    
*   Is the new email verified before it becomes primary?
    
*   Are existing recovery tokens invalidated?
    
*   Does the application require step-up authentication?
    

In this case, changing the primary email immediately redirected the normal password-reset process to the attacker.

* * *

### Keep Testing After Submission

The original report was already valid.

It exposed sensitive PII belonging to other users.

But continuing to test the surrounding workflow revealed the Critical impact.

Sometimes the strongest escalation is discovered after the initial report has already been submitted.

The important part is to:

*   stay within the program rules
    
*   use researcher-controlled accounts
    
*   avoid changing real user data
    
*   document each step clearly
    
*   submit the additional evidence promptly
    

The final report told a very different story from the first observation.

* * *

### Retest the Original Attack Chain

A fix should not be accepted because a new security control appears somewhere in the interface.

The correct retest is to repeat the original attack from beginning to end.

In this case, that meant confirming that an attacker could no longer:

1.  Load another account’s editable profile.
    
2.  Change its primary email address.
    
3.  Redirect password-reset messages.
    
4.  complete the takeover.
    

After OTP verification was introduced, the previous chain no longer worked.

Only then did I confirm the remediation.

* * *

## Final Thoughts

The vulnerability began with one client-controlled parameter:

```text
userid=[account name]
```

Changing it caused the server to load another user’s complete profile.

Following the same account reference through the update workflow allowed the primary email address to be replaced.

The password-reset feature then turned that unauthorized modification into a full account takeover.

The final chain was:

```text
Client-controlled userid
          ↓
Cross-account profile loaded
          ↓
Personal, business, and address data exposed
          ↓
Primary email changed
          ↓
Password reset redirected to attacker
          ↓
New password created
          ↓
Zero-click account takeover
```

The main lesson is simple:

> When an IDOR appears inside an account management workflow, do not stop after proving that you can read another user’s data.

The exposed profile may only be the first part of the vulnerability.
