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 lab’s public POST /forgot-password handler calls an internal route shaped as /api/internal/v1/users/{username}/field/email. Because the submitted username is concatenated into that path, dot segments can change the route and %23 can discard the suffix appended by the gateway.

The REST-path objective is to retrieve `passwordResetToken`, access the administrator account, and delete `carlos`.
The REST-path objective is to retrieve `passwordResetToken`, access the administrator account, and delete `carlos`.

Lab objective/setup

Use POST /forgot-password to test how username is concatenated into the internal REST path. The objective is to traverse from the appended user segment to the v1 passwordResetToken field, reset the administrator password, and delete carlos.

What I observed

Query-string separators such as & and ? produced Invalid Route, indicating that the whole value was being treated as a path component rather than parsed as another query parameter. By contrast, ./administrator retained the normal response, ../administrator changed route matching, and excessive traversal produced a 404 that referenced the API definition. Together, those responses showed that the backend normalized REST path segments.

The password-reset workflow submits to `POST /forgot-password` and loads a client reset script.
The password-reset workflow submits to `POST /forgot-password` and loads a client reset script.
Appending `%23` to `administrator` produces `404 Invalid route` and an API-definition clue.
Appending `%23` to `administrator` produces `404 Invalid route` and an API-definition clue.
A query-style `%26field=x%23` probe remains part of the REST path and does not create a backend query parameter.
A query-style `%26field=x%23` probe remains part of the REST path and does not create a backend query parameter.
A trailing encoded ampersand is treated as part of the username, confirming REST-path handling.
A trailing encoded ampersand is treated as part of the username, confirming REST-path handling.
The `./administrator` probe normalizes to the original user path and returns the normal email response.
The `./administrator` probe normalizes to the original user path and returns the normal email response.

Why it is vulnerable

Concatenating untrusted input into an internal path allows the caller to change routing semantics after the public endpoint has accepted the request.

Walkthrough in the authorized PortSwigger lab

  1. Submit the forgot-password request and send it to Repeater.
  2. Compare username=./administrator, username=../administrator, and username=../../../../. The different responses confirm path normalization and provide the API-definition clue.
  3. Request GET /api/openapi.json. The schema documents /api/internal/v1/users/{username}/field/{field} and identifies passwordResetToken as an available field.
  4. Submit username=../../v1/users/administrator/field/passwordResetToken%23. After normalization, the internal request targets the administrator token while the decoded # prevents the automatically appended /field/email suffix from affecting the route.
  5. Use the returned token in the lab’s reset-password flow, sign in as administrator, and delete carlos to meet the Academy objective.
1
2
3
username=./administrator          → normal response ("If that account exists…")
username=../administrator → Invalid route
username=../../../../ → 404 Not Found
1
GET /api/openapi.json
1
2
3
4
username=../../v1/users/administrator/field/passwordResetToken%23

Normalized internal path:
/api/internal/v1/users/administrator/field/passwordResetToken#/field/email
The `../administrator` probe changes route resolution and produces the API-definition hint.
The `../administrator` probe changes route resolution and produces the API-definition hint.
Excessive dot-segment traversal leaves the internal API route and triggers an upstream `Not Found` response.
Excessive dot-segment traversal leaves the internal API route and triggers an upstream `Not Found` response.
The `username=../../../../` probe reaches beyond the internal API route and returns an upstream `500 Not Found` response.
The `username=../../../../` probe reaches beyond the internal API route and returns an upstream `500 Not Found` response.
Requesting `openapi.json` through the constructed path leaks the OpenAPI document in the error body.
Requesting `openapi.json` through the constructed path leaks the OpenAPI document in the error body.
A field-path probe reaches a version that restricts reads to `email`, revealing version-specific behavior.
A field-path probe reaches a version that restricts reads to `email`, revealing version-specific behavior.
The client script identifies `passwordResetToken` as the password-reset URL parameter.
The client script identifies `passwordResetToken` as the password-reset URL parameter.
Reading `passwordResetToken` on the current API version is blocked, so the path must be rewritten to another version.
Reading `passwordResetToken` on the current API version is blocked, so the path must be rewritten to another version.
The normalized v1 path returns the administrator's `passwordResetToken` in a `200 OK` response.
The normalized v1 path returns the administrator's `passwordResetToken` in a `200 OK` response.
The leaked `passwordResetToken` opens the REST-lab administrator password-reset form.
The leaked `passwordResetToken` opens the REST-lab administrator password-reset form.
After the administrator reset and deletion of `carlos`, the Academy marks the lab solved.
After the administrator reset and deletion of `carlos`, the Academy marks the lab solved.

Verification

The administrator reset token is returned by the normalized v1 field path. Using it to reset the password and delete carlos changes the Academy state to solved.

The final lab state is `Solved`.
The final lab state is `Solved`.

Impact and defensive lesson

The lab demonstrates account takeover when untrusted input is concatenated into an internal REST path. Build backend requests with fixed route templates and validated path parameters, reject path separators and dot segments where usernames are expected, avoid relying on fragment behavior to sanitize a constructed URL, and restrict sensitive internal fields independently of gateway routing.

References