Post by alanms » Sun Sep 19, 2010 9:02 am

(versions up to and including a clean install of 1.4.9.1)
Hi guys,

While working on a module based on coupons, I found (and found fixes for) a moderately serious possible exploit in the coupon system.

This is not too big a deal for most shops, unless your shop offers generous limited-use coupons, in which case it could be very serious.

This is a public forum, so I won't go in to too much detail. Essentially, a malicious or opportunistic customer with a generous restricted-use coupon can discover a way to re-use it an unlimited number of times for one product type in any payment method and regardless of logged-in state. On Paypal shops, the problem is especially serious (any number of products!), and worryingly easy to stumble on by accident.

More worryingly, I believe (I'm no expert) that in some countries, the confirm page and confirmation email legally constitute a contract, meaning some shops might not legally be able to challenge this kind of coupon abuse. They might therefore be compelled to pack and ship 500 £15 + £3 shipping t-shirts bought for £0.00 each all on the same single-use £20 coupon...

The cause is, the fact that new order_ids are created each time the 'confirm' page is loaded, plus the fact there's no validation of coupons or update to their redemption status between the confirmation page and payment. This should be enough for the devs here to figure out how to replicate the problem (hint: ctrl-T).

I spent all afternoon on this, and found two possible fixes: one good, one pretty bad but maybe better in some rare cases.

------------
The 'good' fix is also the easiest: simply apply QPhoria's patch to order.php preventing new order IDs from being created - see http://forum.opencart.com/viewtopic.php?t=9192&f=23

Easy, job done! This should seal the problem completely on every payment method, and in my testing, did so on all those I tested, including PayPal.

As you can see from the linked discussion, many people have used this patch for other reasons without trouble. In my testing, I could only find one minor issue (this occurs in some unknown, unlabelled flavour of OpenCart 1.3).If someone checks out one tab using one payment method, then checks out another tab using PayPal, money is taken from their PayPal account, they are sent a 'success' confirmation email, but no change occurs to the Order table on the DB. The ID of their PayPal purchase matches that of their other purchase (therefore legally just one contract between buyer and seller? Don't quote me on that!). This should be a minor and unlikely scenario -if someone pays you twice by mistake or misadventure on one order ID, you can see by cross-referencing the order number in the PayPal email what has happened and can re-fund them if they complain.

--------------------------------------------------------

There's also a 'bad' fix, which I developed while I thought that generating new order_ids on each load of the 'confirm' page was a deliberate essential feature of OpenCart. This can be explored if for any reason you need to keep pumping out order_ids each time the confirmation page loads.

Basically, you need to find a way to sneak something like the following code in between the customer hitting 'Confirm Order' and the chosen payment controller loading:

Code: Select all

//THIS CODE IS UNTESTED ----------------------------

        if ($this->session->data['coupon']){
          $coupon_query = $this->db->query("SELECT uses_customer, coupon_id FROM " . DB_PREFIX . "coupon WHERE code = '" . $this->session->data['coupon'] . "'" ));
              $coupon_customer_uses_query = $this->db->query("SELECT COUNT(*) AS total FROM `" . DB_PREFIX . "order` WHERE order_status_id > '0' AND coupon_id = '" . (int)$coupon_query->row['coupon_id'] . "' AND customer_id = '" . (int)$this->customer->getId() . "'"));
              $coupon_total_uses_query = $this->db->query("SELECT COUNT(*) AS total FROM `" . DB_PREFIX . "order` WHERE order_status_id > '0' AND coupon_id = '" . (int)$coupon_query->row['coupon_id'] . "'"));
              if ($coupon_customer_uses_query->row['total'] >= $coupon_query->row['uses_customer']||$coupon_total_uses_query->row['total'] >= $coupon_query->row['uses_total']) {
                unset($this->session->data['coupon'];
                  $this->redirect($this->url->https('checkout/confirm'));
                  // Move this string to the appropriate language file... 
                  $this->session->data['error'] = "Your discount coupon has been used its maximum number of times while you were completing checkout, and is no longer valid";
                }
        }
        
        // Any other intermediary processing here...
        
        // If we're okay to proceed to payment method:
      $this->load->model('checkout/order');
      $this->model_checkout_order->update($this->session->data['order_id'], 1);
 

This is not at all a trivial task, as the submit button goes straight into one of many payment methods, with no common code we can hook into - not even a common 'PaymentController' class. Before I found that preventing proliferation of order_ids was an option, the method I was exploring was to print an AJAX call to this PHP code into the HTML of the confirm/guest_step_3 pages ahead of the AJAX form submit, and then also call this code from the noscript alternative (controller/checkout/payment.php I believe?). Nothing else I tried came close to working.

Any approach like this also has two fairly major but seemingly unavoidable negative side-affects for the whole shop, stemming from that line about $this->model_checkout_order->update($this->session->data['order_id'], 1);

This is essential to fix the vulnerability, else a customer can rack up any number of PayPal login pages for the same coupon on different order ids, then work through them one at a time safe in the knowledge that PayPal will never question the accuracy of the total it has been sent.

There's two big problems it causes, for all customers to the shop:
1: Any customer trying to redeem a hard-earned one-use coupon through PayPal, who gets distracted or changes their mind mid way through the PayPal process and starts again, will lose their coupon forever. If a shop is trying to reward loyal customers, this is bad marketing!
2: Any customer who abandons an order mid-way through payment will have their status as 'pending'. This might cause confusion for shop administrators if 'pending' is used significantly in a payment process.

So, this type of fix is very much not recommended, unless generous single-use coupons are essential to your business model and for some reason you need new order ids to be created each time 'confirm' loads.

The first fix, however, seems to just work. Since it has a second benefit of reducing clutter in order reports, it seems a good idea to move it into core.

Newbie

Posts

Joined
Fri Sep 03, 2010 10:01 am

Post by Daniel » Sun Sep 19, 2010 6:28 pm

stop wasting my time with this. you have not found anything important.

this is the way its sopposed to work and how many coupon systems oinb big name stores work!. you can not stop a customer opening another browser and using the coupon again while his first browser is on the payment page.

If i stopped a coupon once it had been validated but the order did not complete then the coupon system would not work very well. People would complain that there coupon does not work even though they have not completed an order.

You just cancel the customers order insead of sending it out.

OpenCart®
Project Owner & Developer.


User avatar
Administrator

Posts

Joined
Fri Nov 03, 2006 6:57 pm

Post by alanms » Tue Sep 21, 2010 4:44 am

Daniel wrote:stop wasting my time with this. you have not found anything important.
No need to be rude, this is a bug report, not a personal attack. I said it's only a big deal for certain types of store.
Daniel wrote:You just cancel the customers order insead of sending it out.
For Mom & Pop stores of with a few orders a day and catalogs that wouldn't attract any serious scammers, you're right, this is sufficient.
Daniel wrote:you can not stop a customer opening another browser and using the coupon again while his first browser is on the payment page.
That's true of PayPal and other external multi-step payments, and allows someone to re-use a coupon as many times as they have browsers (or more, if they suppress or clear cookies).

It's fixable with coupon instantiation, and that's what I've done with my module which hooks up with a 3rd party loyalty scheme who have a discount instantiation already covered. Something like this could be done in Opencart, but yeah, it's probably not worth the trouble. Save it for the big guys.
Daniel wrote:If i stopped a coupon once it had been validated but the order did not complete then the coupon system would not work very well. People would complain that there coupon does not work even though they have not completed an order.
Couldn't agree more - that's why I say the second option's pretty rubbish.

This isn't true of Qphoria's order id patch however. The end result is intuitive: your orders merge smoothly across all tabs or windows, coupons included, and the only way a customer can inconvenience themself is to check out the same order twice.
Daniel wrote:this is the way its supposed to work and how many coupon systems oinb big name stores work!.


I've never met anyone who worked in Amazon's "Deleting Duplicate Order Records / Manually Checking Discount Coupons Department"... but I guess stranger things have happened!

Newbie

Posts

Joined
Fri Sep 03, 2010 10:01 am
Who is online

Users browsing this forum: No registered users and 34 guests