---
title: free food from a receipt
description: how a friend's receipt lead to a vulnerability disclosure
published: 2026-07-12T00:00:00.000Z
updated: 2026-07-12T00:00:00.000Z
tags:
  - security
  - web
draft: false
signature: ./free-food-from-a-receipt.md.asc
---

# introduction

---

i have a friend who really likes to share the food he eats. to the point where it gets annoying. whenever he goes out, i get a photo of the food, sometimes the menu, and if im especially lucky, the receipt too

some time ago he went to an asian restaurant chain with a bunch of branches across the uk. the usual photo arrived, followed by the receipt. this time the bottom of it had a loyalty code which could be entered online to collect points

![photo of the meal my friend sent](/blog/free-food-from-a-receipt/photo_food.jpg)

![photo of their paper receipt](/blog/free-food-from-a-receipt/photo_receipt.jpg)

naturally, i decided to do a little trolling and redeem it before he could

so i opened the restaurant's website, made an account, entered the code and got 18 points. that should have been the end of it, but the website felt kinda old. not necessarily bad, just old in a way that makes you wonder whats behind it

it turned out the restaurant was a customer of a uk hospitality software provider. the customer-facing website was their cic (customer interaction centre), with the restaurant's branding put on top

# looking around

---

the frontend was a react spa bundled with webpack and using axios for its api calls. static files came from cloudfront, while the actual site sat behind aws elastic load balancing and an apache backend on ec2. the exact apache and backend versions weren't exposed, which is good, but the rest of the structure was fairly easy to follow from the javascript bundle

at this point my curiosity took over and i didn't notice how i was already trying different loyalty codes. the input accepted pretty much anything because client-side validation only checked whether the field was empty. the actual validation happened in an api request, with the code placed directly in the url

as expected, random guesses didn't get me anywhere. after a few attempts the server returned `429` and told me that too many vouchers had been redeemed in five minutes. so direct guessing was rate limited, which was exactly what i expected

for a moment i thought the frontend might still reveal the exact format. if it had a strict regex, fixed prefixes, checksums or even just a useful length check, i could at least understand how the codes were generated without sending more requests

it didn't. there was only a required field

but while searching the minified source i found something much funnier: a hard-coded example code. entering it didn't contact the server at all. it just returned this message locally:

> Success! 42,000 points added... Just kidding. That's an example code.

![screenshot of the minified code](/blog/free-food-from-a-receipt/screenshot_42k_points.png)

# the receipt

---

i kept digging through the frontend. there were endpoints for customer details, baskets, menus, loyalty rewards, orders and receipts. most of it looked normal. some things returned generic errors, some needed a valid session, and the loyalty endpoint had its rate limit

then i noticed the "my transactions" section. every transaction had a view button which showed a digital version of its receipt

this wasn't an image or a pdf. clicking view made a request with two numbers: the receipt id and venue id. the api returned a json string containing the complete receipt as html, and react inserted that html into the page with `dangerouslySetInnerHTML`

![screenshot of the account transaction page](/blog/free-food-from-a-receipt/screenshot_transactions_page.png)

![screenshot of the digital receipt displayed inside](/blog/free-food-from-a-receipt/screenshot_receipt_view.png)

that was interesting for two reasons. first, receipt ids looked numeric. second, the digital receipt contained the same loyalty code at the bottom as the paper one

this entire time i was still in a discord call with the friend who sent it. not even half an hour had passed since the original photo

so i had a hypothesis: maybe the receipt request only needed those two numbers. maybe the account page itself was the only thing stopping you from viewing somebody else's receipt

there was one easy way to find out...

# one thing leads to another

---

i requested the receipt i already owned without sending my session cookie. it returned `200` with the complete receipt

then i changed the receipt id by one... another 200... and another receipt...

![screenshot of terminal output curl'ing several receipt ids](/blog/free-food-from-a-receipt/screenshot_curling_receipts.png)

the ids were sequential, the endpoint didn't require authentication, and it didn't check whether the receipt belonged to me. in a small range from `19990` to `20010`, every id returned a receipt. eighteen of the twenty-one receipts included a loyalty code at the bottom

the original rate limit suddenly didn't matter much. i didn't have to guess codes anymore. the system itself was giving me valid ones

it took about a minute to write the proof of concept. it looked something like this:

```txt
for receipt_id in range(19990, 20011):
    receipt = fetch receipt
    code = regex extract loyalty code from receipt html

    if code exists:
        redeem code w/ my credentials
```

![screenshot of the proof-of-concept output showing loyalty codes and credited points](/blog/free-food-from-a-receipt/screenshot_poc.png)

i limited it to five successful redemptions. i didn't wanna keep taking codes from real receipts just to make a point i had already proven

it worked immediately:

```text
receipt 19991 -> +36 points
receipt 19992 -> +35 points
receipt 19993 -> +36 points
receipt 19994 -> +29 points
receipt 19996 -> +31 points
```

the next attempts returned `429` with `too many vouchers redeemed in 5 minutes`, but by then the account had already gone from 18 points to 185

![screenshot of the loyalty rewards page](/blog/free-food-from-a-receipt/screenshot_rewards.png)

those points remained on my account. the restaurant's loyalty scheme gives out free dishes, drinks and other rewards as the balance grows, so this wasn't just a number changing on a profile. the chain ended with something that could be exchanged for actual food

and this was only the loyalty side of it. each exposed receipt also contained the venue, time, order contents, amount paid, server name and other transaction details

# disclosure

---

i reported the issue privately to the provider with the affected tenant, the two relevant api behaviours, a minimal proof of concept, redacted output and the exact change to my points balance

i also suggested requiring authentication and receipt ownership checks, replacing predictable receipt urls with access tokens, removing loyalty codes from unauthenticated receipt responses, and applying limits to receipt retrieval rather than only redemption

![screenshot of the redacted email reporting the issue](/blog/free-food-from-a-receipt/screenshot_disclosure_email.jpg)

its now been a few weeks and i still haven't received a response :c

# conclusion

---

all of this started because my friend wouldn't stop sending photos of his food

less than half an hour after he sent one receipt, i had gone from stealing his 18 points as a joke to watching my account fill itself from other people's receipts

it was a fun "eureka" moment for about five seconds, followed by the much less fun realization that i will have to write an email disclosing it now...
