04 April 2016
Note: Domino's have since resolved the issue and is one of the reasons why I've decided to post this article. Payments are still being processed client side but they now have the proper server side checks in place.
Friday evening, circa 3 years ago. I'm craving an Americano with extra pineapple and hot dog stuffed crust. I fire up the Domino's Android app, place my order and 40 minutes later I'm stuffing my face with 13.5" goodness.
I've ordered enough pizza to know that at the end of the order process you sometimes, seemingly randomly, get a £10 off voucher code for your next order. Naturally, I was intrigued to how this was generated.
After sifting through the apps source code I notice that the code is generated server side via an API call. I fire up a proxy (Burp) to monitor the web traffic between my phone and the Domino's API server and run through the order process. Something immediately catches my eye...
The Domino's app itself was processing payments client side via a payment gateway.
This isn't inherently bad if it has been correctly implemented with the appropriate server side checks. Usually payments would be processed server side so that the process is hidden and out of the hands of meddling users.
So let's take a closer look. I place a new order with the VISA debit card test number (4111111111111111) which returns the following response from DataCash (the payment gateway):
<Response> <CardTxn> <authcode>NOT AUTHORISED</authcode> <card_scheme>VISA</card_scheme> </CardTxn> <datacash_reference>3340105259009953</datacash_reference> <merchantreference>3340105259009953</merchantreference> <mode>LIVE</mode> <reason>DECLINED</reason> <status>7</status> <time>1449024000</time> </Response>
As expected the card is declined and the App shows an error message. Let's try our luck by intercepting the response and changing some values around. I start a new order and set breakpoints on the HTTP endpoint for the DataCash API. Once the breakpoint triggers on the response, I change the
<reason> attribute value to ACCEPTED and
<status> to 1 (which means transaction accepted according to the DataCash documentation).
Errr, what? It looks like my order was placed without a valid payment. Surely this is an oversight/edge case and Dominos's will have back office checks in place before physically starting to prepare my order... right?
A few minutes pass and the Pizza Tracker changes from "Order" to "Prep" and then to "Baking". I couldn't bear to wait another 30 minutes to see if an Americano pizza, Chicken Strippers and Chocolate Chip Cookie + Ice Cream side turn up at my door. I called the store and they confirm they have received my order and it will be delivered within the next 20 minutes. My first thought: awesome. My second thought: shit.
The pizza arrives and I tell the delivery driver there must have been a mistake with the order as I never entered any card details and wanted to pay with cash. He happily leaves with £26 and my conscience is clean.
Let's take a look at what happened. Essentially the App's logic boils down to (pseudocode):
if (datacash.response.reason == 'ACCEPTED' && datacash.response.status == 1) placeOrder();
placeOrder() sends an HTTP request to the Domino's API with the
order_id (generated when you start your order) and
<merchantreference> (in the above XML response). All Dominos needed to do was verify the reference server side. But no, let's trust the client. The client never lies.
Payments aside, the moral of the story is to always validate your inputs server side. Always.