Authorization disclaimer. This walkthrough is limited to the intentionally vulnerable PortSwigger Web Security Academy lab linked below. Test only systems you own or are explicitly authorized to assess.

Overview

The public password-reset workflow forwards attacker-controlled data to an internal API query string. Delimiters in the supplied value alter the backend parameter set.

The objective is administrator account access followed by deletion of `carlos`.
The objective is administrator account access followed by deletion of `carlos`.

Lab objective/setup

Use POST /forgot-password to determine how the supplied username is inserted into an internal query string. The backend normally returns the requested user’s masked email; the goal is to replace the appended field=email parameter with field=reset_token.

What I observed

The normal request for administrator returns a masked email. An unknown username returns Invalid username; injecting %26x=y returns Parameter is not supported, proving that decoded & creates a backend parameter. Adding %26field=x%23 changes the error to Invalid field, showing that %23 truncates the gateway’s original suffix.

Burp records the session used for the password-reset probes.
Burp records the session used for the password-reset probes.
The password-reset page loads `forgotPassword.js` and submits to `POST /forgot-password`.
The password-reset page loads `forgotPassword.js` and submits to `POST /forgot-password`.
The baseline `administrator` request returns a masked email value with response type `email`.
The baseline `administrator` request returns a masked email value with response type `email`.
A nonexistent username produces `400 Invalid username`, establishing the normal validation response.
A nonexistent username produces `400 Invalid username`, establishing the normal validation response.

Why it is vulnerable

The front end and internal API disagree about where one parameter ends and another begins, so server-side parameter pollution changes the backend request.

Walkthrough in the authorized PortSwigger lab

  1. Send username=administrator%26x=y; the backend reports an unsupported parameter.
  2. Send username=administrator%26field=x%23; the backend reports an invalid field, confirming control of field and truncation of the appended query string.
  3. Fuzz the field name and identify reset_token as a valid response type.
  4. Submit username=administrator%26field=reset_token%23 and copy the returned token.
  5. Follow the client-script reset URL, set an administrator password, sign in, and delete carlos.
1
username=administrator
1
username=administrator%26x=y
Decoded `%26` injects an `x` parameter, and the backend rejects it as unsupported.
Decoded `%26` injects an `x` parameter, and the backend rejects it as unsupported.
The `%26field=x%23` probe controls the backend `field` parameter and suppresses the original suffix.
The `%26field=x%23` probe controls the backend `field` parameter and suppresses the original suffix.
Field-name fuzzing identifies `reset_token` as a valid backend field.
Field-name fuzzing identifies `reset_token` as a valid backend field.
The client script reveals the reset URL parameter name `reset_token`.
The client script reveals the reset URL parameter name `reset_token`.
The final polluted query returns the administrator's `reset_token` directly in the JSON response.
The final polluted query returns the administrator's `reset_token` directly in the JSON response.
The leaked token opens the administrator password-reset form.
The leaked token opens the administrator password-reset form.
After resetting and signing in as administrator, the admin panel exposes the delete action for `carlos`.
After resetting and signing in as administrator, the admin panel exposes the delete action for `carlos`.

Verification

After the administrator password reset and deletion of carlos, the Academy changes the lab state to solved.

The Academy marks the lab solved after `carlos` is deleted.
The Academy marks the lab solved after `carlos` is deleted.

Impact and defensive lesson

Encode user-controlled values before inserting them into backend query strings, construct requests with parameter APIs rather than concatenation, reject duplicate or unexpected parameters, and never expose password-reset tokens through a generic user-field lookup.

References