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
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.