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

Overview

The checkout API exposes an internal discount property when its state is retrieved, then accepts the same property from a client during checkout. Submitting a 100% discount reduces the lab’s jacket order to zero and bypasses the insufficient-funds condition.

The lab objective is to exploit mass assignment during `POST /api/checkout` to purchase the jacket.
The lab objective is to exploit mass assignment during `POST /api/checkout` to purchase the jacket.

Lab objective/setup

Sign in with the supplied wiener:peter account and buy the Lightweight l33t Leather Jacket. The relevant operation is POST /api/checkout; requesting the same resource with GET reveals additional checkout state.

What I observed

Adding the jacket and attempting checkout produced the expected insufficient-funds failure. Burp’s HTTP history identified the checkout API request and its JSON body.

HTTP history identifies both supported methods on `/api/checkout` for comparing readable and writable fields.
HTTP history identifies both supported methods on `/api/checkout` for comparing readable and writable fields.
The baseline `POST /api/checkout` contains only `chosen_products` and redirects with `INSUFFICIENT_FUNDS`.
The baseline `POST /api/checkout` contains only `chosen_products` and redirects with `INSUFFICIENT_FUNDS`.

Changing the method to GET for /api/checkout returned the serialized cart state. In addition to the selected products, the response contained chosen_discount with a percentage value of 0.

`GET /api/checkout` discloses the writable-looking object shape `chosen_discount.percentage` with a current value of `0`.
`GET /api/checkout` discloses the writable-looking object shape `chosen_discount.percentage` with a current value of `0`.

Why it is vulnerable

The server treats a client-supplied object as authoritative without restricting which properties may be bound to the checkout model. The read response discloses a server-controlled field, and the write operation accepts that same field. This combination lets the client assign a business-sensitive value that should be derived and enforced exclusively on the server.

Walkthrough in the authorized PortSwigger lab

  1. Add the jacket to the cart and attempt checkout once to capture POST /api/checkout.
  2. Send the request to Repeater and issue GET /api/checkout to inspect the returned object.
  3. Copy the disclosed property shape into the original checkout JSON and set its percentage to 100:
1
2
3
4
5
6
{
"chosen_products": ["..."],
"chosen_discount": {
"percentage": 100
}
}
  1. Submit the modified POST /api/checkout request. The server accepts the client-selected discount and creates the order.
The modified checkout assigns `chosen_discount.percentage` to `100`; the server responds `201 Created` and confirms the order.
The modified checkout assigns `chosen_discount.percentage` to `100`; the server responds `201 Created` and confirms the order.
The Academy marks the mass-assignment checkout lab as solved.
The Academy marks the mass-assignment checkout lab as solved.

Verification

The checkout response changes from insufficient funds to 201 Created, and the lab records the jacket purchase. This verifies that the additional JSON property, rather than a legitimate coupon workflow, controlled the final price.

Impact and defensive lesson

Mass assignment can expose any writable model property—not only discounts. Account roles, verification state, ownership identifiers, balances, and workflow flags can become security boundaries if blindly accepted from request bodies.

  • Define request DTOs or explicit allow-lists for every write operation.
  • Reject unknown properties instead of silently binding them.
  • Calculate prices and discounts server-side from trusted product and promotion data.
  • Keep response models separate from writable request models; do not assume a field is safe to accept merely because it is safe to display.
  • Add authorization and invariant checks after binding and before persistence.

References

Provenance

Adapted and rewritten in English from the private migration source declared in the front matter. Technical observations and screenshots are retained from that authorized PortSwigger lab session; no behavior beyond the source evidence is asserted.