5

We have encountered a strange problem in the payment module of trigger.io. The flow works perfectly with ios payments but in android, some in-app payment callbacks are called twice in the same second. the receipt signatures are different but the orderid, notificationid, purchasetoken and developerPayload all stay the same. when we try to validate the receipt it turns out to be true and correct. But when we look at the financial report, we only see one payment instead of two (because its probably just one payment but why the different signatures?).

why is trigger.io calling the callback twice which leads to the product being bought twice? why is android returning 2 different and confirmable receipts for one payment? is this a bug on andorid side or trigger.io side, cause i have no way of calling the callback using javascipt? or is this a known hack attempt?

We also encountered a case where no callback was called at all, whereas the credit card was charged successfully. Is this a bug or are there any workarounds for this case?

here is the code i'm initiating a purchase:

if(forge.is.android())
forge.payments.purchaseProduct("someproductname", paymentSuccess, paymentError);

and here is the callback function:

function paymentCallback(data, confirm){

    forge.request.ajax({
        url: "someurl.php",
        dataType: "json",
        data:"function=logPayment&action=PaymentCallbackStart",
        success: function (data) {
            hideLoader();
        },
        error: function (error) {
            hideLoader();
        }
    }); 

    var productId = data.productId;
    var orderId = data.orderId;
    var signed_data;

    if(forge.is.android())
    {
        var state = data.purchaseState;
        var receipt = encodeURIComponent(data.receipt.signature);
        signed_data = encodeURIComponent(data.receipt.data);
    }
    else if(forge.is.ios())
    {
        var state = data.PurchaseState;
        var receipt = data.receipt.data;
    }

    forge.request.ajax({
        url: "someurl.php",
        dataType: "json",
        data:"function=logPayment&data=" +  encodeURIComponent("birthday=" +  gbirthday + "&birthhour=" +  gbirthhour + "&name=" +  gname + "&gender=" + ggender + "&birthday2=" + gbirthday2 + "&birthhour2=" +  gbirthhour2 + "&name2=" +  gname2 + "&gender2=" + ggender2 + "&content=" +  text + "&ProductID=" + qs.ProductID + "&userId=" + guserId + "&data=" + JSON.stringify(data)) + "&action=PaymentCallback",
        success: function (data) {
            hideLoader();
        },
        error: function (error) {
            hideLoader();

        }
    });     

    if(state == "PURCHASED")
    {
        if(typeof gbirthday != "undefined")
        {
            var text = $('#imessagem').val();
            forge.request.ajax({
                url: "someurl.php",
                dataType: "json",
                data:"function=askQuestion&birthday=" +  encodeURIComponent(gbirthday) + "&birthhour=" +  encodeURIComponent(gbirthhour) + "&name=" +  encodeURIComponent(gname) + "&gender=" + ggender + "&birthday2=" +  encodeURIComponent(gbirthday2) + "&birthhour2=" +  encodeURIComponent(gbirthhour2) + "&name2=" +  encodeURIComponent(gname2) + "&gender2=" + ggender2 + "&content=" +  encodeURIComponent(text) + "&ProductID=" + qs.ProductID + "&userId=" + guserId + "&signed_data=" + signed_data + "&receipt=" + receipt,
                success: function (data) {
                    processPayment(productId,orderId)
                    hideLoader();
                },
                error: function (error) {
                    hideLoader();

                    forge.request.ajax({
                        url: "someurl.php",
                        dataType: "json",
                        data:"function=logPayment&data=" + encodeURIComponent(JSON.stringify(error)) + "&action=PaymentQuestionError",
                        success: function (data) {
                            hideLoader();
                        },
                        error: function (error) {
                            hideLoader();

                        }
                    });                     

                }
            }); 

            forge.request.ajax({
                url: "someurl.php",
                dataType: "json",
                data:"function=logPayment&data=" +  encodeURIComponent(JSON.stringify(data)) + "&action=Payment",
                success: function (data) {
                    hideLoader();                   },
                error: function (error) {
                    hideLoader();

                }
            });             
        }
        if(forge.is.android())
        processPayment(productId,orderId);
    }
    else
    {
        if(forge.is.ios())
        processPayment(productId,orderId);
    }
    confirm();
}   
Volkan Ulukut
  • 4,230
  • 1
  • 20
  • 38

1 Answers1

1

This is called a replay attack. Normally you will update your database if you have received a payment(callback, eg. IPN for PayPal). If they call the same order again and again the attack will fail because the status is already set to true.

In earlier days this was a common attack.

Read following articles:

Edit: I guess you do an insert in your database after the callback? It is better to insert the order before the callback(before the actual checkout) and create a status field in your table which is set default to false. When the callback is succeeded you must update the status and set it to true.

Eg. I want to order a pizza(owner puts my order in the system). My receipt is my proof of payment(callback). When the pizza is ready I eat it but I'm still hungry. I go back to the pizzaboy and I ask for a new one(I could repeat this a thousand times). A simple solution would be to destroy my receipt or put a signature on it(update status) and I wouldn't be able to order the same pizza all over again.

Edit edit: When you accept PayPal be aware of the chargeback 'attack'(http://forums.whirlpool.net.au/archive/2214159).

13/05/2014 : The only thing I see at the moment is that your AJAX data property is formatted wrong. This isn't a string but an object. The isn't probably the real problem. If you not always receive a callback and you are sure that your request hits the Google servers I guess it is a problem on their side(or Trigger.IO). I would advice you to contact Trigger.IO to make sure that your request actually hits their servers. If it does, you could contact Google about this problem and see if they receive all of your requests.

forge.request.ajax({
    url: "someurl.php",
    dataType: "json",
    data:{
        function(watchOut!! 'function' is a reserved keyword!!) : 'logPayment',
        action   : 'PaymentCallbackStart'
    },
    success: function (data) {
        hideLoader();
    },
    error: function (error) {
        hideLoader();
    }
}); 
Community
  • 1
  • 1
GuyT
  • 4,316
  • 2
  • 16
  • 30
  • I'm doing an insert before the payment and set it to true along with the signature and order id. the problem is not multiple submission of same purchase request, but the multiple initiation of the callback. I've digged into the issue some more and found out that android is sending the callbacks it couldn't for some reason (connection issues, app crash etc) days before the second purchase, along with the new purchase's callback. so the missing orders appear with a multiple callback initiation. – Volkan Ulukut May 12 '14 at 10:20
  • here is the related issue on google: https://code.google.com/p/marketbilling/issues/detail?id=14&colspec=ID%20Type%20Status%20Google%20Priority%20Milestone%20Owner%20Summary so the question is: is there a known workaround for this problem? since I can't match the purchase (we ask a couple of questions and send them along with the purchase request) if it's sent days later, what should I do to automate the fixing of this issue? – Volkan Ulukut May 12 '14 at 10:24
  • Could you provide some code where you create the call and of the callback function? – GuyT May 12 '14 at 10:54
  • Hmm.. The only thing I see that's wrong is your `AJAX data property`. This is an object and not a string. See mee updated answer for an example. – GuyT May 13 '14 at 06:19
  • although this doesn't solve my initial problem, thanks for the effort. – Volkan Ulukut May 13 '14 at 10:30