IDOR via Base64 Encoded Return Credit Claim

Intermediate IDOR 8 min read

Gearbox lets customers release a rental deposit to store credit after printing a return label. The claim request contains a server-generated reference that is also printed on the label. I changed one return identifier inside that reference, and the endpoint transferred another customer’s $96 deposit into my test account while marking her return as paid.

How it works

1 The lab

Every case takes a real Uphack lab and walks the exact steps that find its flaw, one at a time. The lab itself is waiting at the end.

2 The panel

It plays like a video, except you drive it: scroll, and the panel beside the text walks the screens, the traffic and the code down to the exact line that fails.

3 The evidence

Every screen, request and line of code is taken from the running lab, down to the line numbers. Nothing here is illustrative.

01

The return credit claim flow

Surface

Gearbox rents tools, cameras and camping equipment against a deposit. When a rental ends, the customer starts a return and prints a prepaid label. The application then allows the customer to release the deposit as store credit before a courier collects the item or the warehouse inspects it, so one authenticated request changes the customer’s balance.

The Release my deposit now button initiates that request for Morgan Lee’s pending $38 deposit.

To authorize the transfer, the endpoint must establish which return owns the deposit and which customer owns the receiving wallet. Both decisions must come from the authenticated session and trusted server-side records because the request changes a financial balance.

02

The return reference

Evidence

The return label prints a field called Return reference beside the shipping service and package weight. Because the complete value is visible to the customer, the server must treat it as client-controlled input when the browser submits it later.

The label endpoint supplies the value as returnRef in the same response as the warehouse address and package weight. This response establishes where the browser obtains the reference before making a claim.

Pressing the release button sends the unchanged returnRef to POST /api/returns/claim-credit. It is the request body’s only field, so the endpoint must obtain the target return and the credit recipient from this one value or from the authenticated session.

After Base64-decoding the reference, I found JSON containing two integers. The returnId selects the return whose deposit will be released, while customerId selects the wallet that will receive the credit.

The browser therefore submits both the transaction target and its recipient. The next check is whether the server binds those identifiers to the authenticated customer before changing either account.

03

Ownership enforcement on the read path

Evidence

Using Morgan’s authenticated session, I requested return 4100 from the read endpoint and received the same 404 response that an unknown return would produce.

This response shows that the read path scopes its query to the authenticated customer. A return owned by another customer is unavailable through Morgan’s session, which establishes the expected authorization behavior before inspecting the claim path.

04

The missing session ownership check

Mechanism

The claim handler first calls requireApiCustomer and returns 401 when no authenticated customer exists. It then calls claimReturnCredit with only the submitted reference, leaving the resolved customer out of the authorization decision.

claimReturnCredit rejects return identifiers below 4100 with a message that the credit has already been issued. This guard restricts the accepted identifier range, but it does not verify that the selected return belongs to the authenticated customer.

The function then loads the return with findUnique using only the decoded primary key. Because the query has no customer constraint, any matching return can reach the subsequent state checks.

The function also loads the recipient from the decoded customerId. It verifies that the return has pending credit, then uses the two client-submitted identifiers to select the deposit and recipient without comparing either record with the authenticated session.

05

The server-generated reference remains client-controlled

Mechanism

The application already implements the required ownership constraint in getOwnedReturn. This function includes both the customer and return identifiers in the where clause, so the label page and read endpoint return 404 for return 4100 under Morgan’s session.

The same function generates the reference after confirming that the return belongs to the customer, using the verified return and customer identifiers. The implementation appears to assume that anyone who later submits a valid reference must have obtained it through this ownership-scoped query.

That assumption fails because the browser receives the reference and can decode, modify and re-encode it. The claim endpoint must perform its own ownership check at the point where it changes the wallet and return state.

06

A modified return reference

Proof

I decoded Morgan’s reference, changed returnId from 4102 to 4100 while keeping customerId 1007, and encoded the JSON again. I then submitted it through the same endpoint with the same authenticated session, isolating the return identifier as the only changed authorization input.

The server returned 200 with a receipt for $96 and a closing balance of $108.50. Morgan’s own pending deposit is $38, so the different amount proves that the endpoint released credit from another customer’s return into Morgan’s wallet.

07

Wallet and return state after the claim

Proof

Before the modified claim, Morgan’s wallet contained $12.50 from one counter adjustment associated with the driver kit he rented.

After the claim, the wallet shows receipt #40663 for a $96 deposit released from return #78416. The receipt identifies an REI Co-op Half Dome 2 camping kit that Morgan never rented, confirming the cross-customer transfer in the application’s persistent records.

Return 78416 belongs to Nora Patel. The modified claim marked her return as credit issued and recorded its deposit as released to Morgan, while Nora’s store credit remained $0.00. The return row preserves both the owner and credited customer, which makes the unauthorized transfer traceable after it occurs.

Repeating the same claim returns 409, confirming that the first request caused a persistent state transition. Because Nora’s return is now recorded as paid, she can no longer claim its deposit through the normal endpoint.

Any authenticated customer can apply the same modification to a pending return with an identifier of 4100 or above. The attack requires no administrative role, leaked token or timing condition.

08

Enforce ownership using the authenticated session

Remediation

Base64 changes the representation of bytes without providing integrity, confidentiality or a binding to the session that received the value. Once the browser receives the reference, the customer can submit any decoded identifiers that produce valid encoded JSON.

The corrected handler accepts a plain return identifier because encoding does not contribute to authorization. It uses the authenticated session to determine whether that customer owns the return and which wallet may receive the credit.

The handler resolves the submitted return identifier through getOwnedReturn, reusing the ownership-scoped read from the rest of the application. A return belonging to another customer does not resolve, and the handler selects the receiving wallet from the authenticated customer.id instead of a request-body field.

The return schema already stores the credited customer separately from the owner, so it can represent a transfer to a different account. The handler therefore needs an explicit rule that requires the credited customer to match the authenticated owner for this workflow.

Similar authorization failures can affect checkout or cart tokens used with another basket, export and report job identifiers, one-click links that contain an account identifier, and payment requests that name both the source and destination. When reviewing these endpoints, decode client-visible references and test whether the server accepts them under a session different from the one that received them.

gearbox.returns.example

Based on a real lab.

Every screen and request above came from the live app. In the lab you do the whole thing yourself, hands-on, with guidance along the way.

Intermediate IDOR ~30 min