Post

Coop’s IDOR Vulnerability

Last March I’ve discovered and reported a vulnerability on Coop’s websites (Coop is a major grocery store chain in Switzerland). I found out that the chains’ receipts are publicly visible. Coop had an IDOR vulnerability: anyone in Switzerland that knew a receipt’s barcode could see that receipt. The receipts were also guessable. As they did not contain any significant randomization, it was possible to search for valid barcodes and get arbitrary receipts, e.g., all receipts from a given shop. I’ve published a proof of concept for this attack on GitHub, and in the month following the disclosure, Coop has addressed reported issues.

IDOR vulnerability

the digital receipt service and a receipt the digital receipt service and a receipt

Coop has a loyalty program, Supercard. Shoppers with Supercard can opt out of paper receipts and instead receive their receipts digitally through supercard.ch presented in the picture above. The receipts are given to the user as a PDF file and available under a URL like this: https://www.supercard.ch/bin/coop/kbk/kassenzettelpoc?barcode=9900120012340012300003601234&pdfType=receipt.

You can notice that the URL contains only the receipt’s barcode. I was implementing an automated receipt fetcher and to check what I needed to do to access the PDF, I opened the URL in incognito mode and was surprised to see my receipt. It meant that my authentication cookies from supercard.ch were not needed to access that file. After sharing the link with friends, I’ve concluded that anyone in Switzerland can see other people’s receipts.

The receipt itself has plenty of personal data that enables identification:

  • the shopping location and time
  • payment method used and the last digits of a credit card if used
  • 8 digits of the supercard number and the current point status

Forced browsing

The IDOR vulnerability is exacerbated by the barcode structure, which is non-random and searchable. We can somewhat effectively search for receipts for which we do not have the specific barcode. To understand this, we need to see how a barcode is constructed by looking at some sample barcodes I’ve gathered (decomposition mine):

1
2
3
4
5
6
PREFIX  TRANSACTION ID  DATE    TOTAL    SHOP ID
990     023 00691       250322  0013440  1915
990     024 03817       250322  0003835  1915
990     027 01866       260322  0002990  1915
990     006 01920       250322  0001440  1427
990     006 01346       240322  0001195  1427

Transaction ID appears to be sequential per shop. If 02403817 is a valid ID, we can fetch the previous receipt, which would have 02403816 as its ID. So if we wanted to get a previous shopper’s receipt, the only thing we would need to do is search over possible totals. The iteration over totals is easier in Switzerland as all prices need to be divisible by 0.05 (5 Rappen).

This is forced browsing in a nutshell. One could fetch all receipts from a given day using a browsing function like the one below.

1
2
3
4
5
6
7
8
def browse_receipts(date, shop):
  """Saves receipts generated at that date in the shop."""
  for transaction_id in generate_sequential_ids():
    for total in generate_totals():
      pdf = fetch_receipt(date, shop, transaction_id, total)
      if is_valid(pdf):
        save(pdf)
        break

I have implemented a working proof of concept that illustrates the principle. I haven’t tested how many receipts we could realistically browse through. The only limit, however, is the server’s available throughput. The proof of concept can get a receipt with a particular ID within a minute, so it should be possible to get a significant chunk (a thousand per day) of a shop’s receipts.

This post is licensed under CC BY 4.0 by the author.