Dunicot A cybersecurity consultancy and advisory firm.

Business Logic · Case Study · 8 min read

Nothing was malformed and the money still left

Every request we sent to this checkout was valid. The platform accepted all of them, logged all of them, alerted on none of them, and lost money on each one.

The client ran a regional e-commerce platform with its own wallet, its own promotion engine and a returns process that had grown one exception at a time over six years. They came to us after their finance team noticed that promotional spend for one campaign had exceeded its budget by a margin nobody could explain from the campaign rules.

They suspected fraud rings. What they had was a checkout that did exactly what it had been told to do.

How we scoped it

We asked for one thing before testing started: the state machine. Not documentation, just a whiteboard photo of how an order moves from created to paid to shipped to delivered, and every way it can go backwards. Cancelled, refunded, partially refunded, returned, disputed.

It took their lead engineer twenty minutes and it shaped the whole engagement. Almost every finding we report in commerce sits on a transition rather than on an endpoint, because the endpoints get reviewed and the transitions between them belong to nobody.

Finding one: promotions that stacked

The rules said one promotional code per order. The validation enforced it, correctly, at the point the code was applied. It did not re-run when the cart changed.

So we applied a code, added an item, applied a second code, and removed the first item. The order carried both discounts. Neither request was unusual and both were things a real customer does by accident. We repeated it to four codes before we stopped, at which point the order total was below the cost of shipping it.

Finding two: the refund race

The wallet refund path checked the order status, credited the wallet, then marked the order refunded. Three steps, no lock.

We sent two refund requests for the same order within the same few milliseconds using single-packet delivery. Both read the status before either wrote it. Both credited. The order was refunded once and paid twice.

We then confirmed the credit was real by spending it, on a second account we controlled, on a low-value item we then returned. The money was genuine.

Finding three: cancelled orders that still credited

A cancellation before dispatch released the reserved stock and issued a wallet credit. A cancellation after dispatch was supposed to route through returns instead. The check compared a dispatch timestamp that was written by the warehouse system asynchronously.

Cancelling during that window, which was between four and nine seconds in their environment, produced both outcomes. The item shipped and the wallet was credited.

What none of this looked like

No payload was malformed. No injection was attempted. No authentication was bypassed. Every request came from a logged-in customer doing something the interface allows, in an order the interface did not anticipate.

Their WAF logged nothing because there was nothing to log. Their scanner had run monthly for two years and had never reported any of it, correctly, because a scanner has no model of what an order is supposed to mean.

Re-validate promotions against the final cart at the point of payment rather than at the point of application. Put a lock and an idempotency key on every wallet-crediting path. Derive order state from one authoritative source rather than from a timestamp written by a system that is allowed to be late.

The client shipped all three within a sprint and a half. We retested each finding in a clean session, and the refund race specifically we attempted three hundred times before we accepted it as closed, because a race that fails twice is not fixed.

In short

Point 1
Map the state machine before testing. The findings live on the transitions, not the endpoints.
Point 2
Validate at the point of consequence, not at the point of entry. A check that ran earlier is not a check.
Point 3
Every balance-changing path needs a lock and an idempotency key, without exception.
Point 4
A clean scanner report on a commerce platform tells you the scanner ran. It tells you nothing about the margin.

Want this applied to your stack?

Everything written here comes out of delivered engagements. Describe the platform and the deadline.