Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trigger.IO in-app payments duplicate callbacks same orderid with different signature

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();
}   
like image 598
Volkan Ulukut Avatar asked Nov 10 '22 08:11

Volkan Ulukut


1 Answers

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:

  • http://en.wikipedia.org/wiki/Replay_attack
  • How do I prevent replay attacks?

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();
    }
}); 
like image 109
GuyT Avatar answered Nov 15 '22 01:11

GuyT