I love building things on the internet, because people will interact with your stuff. They’ll mess with it through any and all mediums that someone creative might find. Exposing an API? Someone will misuse it! You’ll fix it, only for them to discover a different hole. You’re playing a game with a person you’ve never seen, only piecing together an idea of who they might be and what they’re like.

In this particular case, I discovered the lonely campaign of a user against their least favourite dish, flooding my feedback poll with negative votes.

Nostalgia feels like the Wrong Word when you’re still Young

I re-visited my school recently to pick up a few documents after graduation. While waiting, I found a sticker, hiding on the far end of one of the outdoor lunch tables. Somehow, traces of a project I had led in the student council a few years ago were still left. What I had found were the last remnants of my first website that saw real users.

Sticker asking for ratings from the students with a QR code linking to the site.

During my tenure in the student council there was a shift towards a more vegetarian regime, always offering at least one vegetarian option and introducing no-meat days. Some people complained very vocally about the new policy, so our canteen’s direction wanted to gauge the students’ reactions to the new menus.

Instead of going for a simple vote, I saw the perfect opportunity to put my - then very new - programming skills to some use and build a website. After some hiccups, the site ran for 2 months and collected a total of 337 votes. In the grand scheme of things, that wasn’t a lot of people - but I had fun and learned a lot.

Minimal Infrastructure

I built the site completely in Typescript, using NextJS for the front-end and ExpressJs on the back-end. Because this was supposed to be temporary and I had no idea how much data I would end up with, I went with a NoSQL Firebase solution. In hindsight, I worried way too much about scale and went for server-less hosting on Google Cloud. This also made sense because voting was only open from 11:50 to 13:50 every day.

The server-less approach paid off - quite literally - and I forked over a total of 3.11€ to Google Cloud, excluding the domain.

How to collect Feedback

After scanning one of the QR codes placed all over the canteen, students would access the following portal. They would select the menu they choose today - one of the 3 menus offered that particular day. They would then rate if from 1-5 and finally select which grade they are in.

Walk through of the app from a user’s perspective.

In order to incentivise people to vote everyday, they needed to get something back from the site too. After rating today’s food, students were then able to see what others thought and how the votes evolved live. This also created some fun discussions when opinions on certain meals differed wildly.

Dashboard showing some meal’s statistics.

Some security Measures

While this was a school website, I still considered the threat model of a technically versed student trying to fuck with my results. Also, even more benign user interactions like accidental repeated voting should not be counted. I was proven right when I later discovered evidence of someone actually trying to manipulate the results.

I couldn’t require login, as this would increase friction too much and nobody would vote. So I had to find another way to block people from voting twice, both from their device and through the API. Preventing API access could have been accomplished - admittedly only by defence in depth - through a CSRF token loaded when requesting the page. But since I had split front-end and back-end, this would have been too complicated for what it was worth.

My first line of defence against people getting creative in order to vote twice was to simply store a cookie containing a UUID after they voted, expiring the next day. But instead of notifying them that they had already voted, I discarded it silently on the back-end. This would give those that simply wanted to try manipulating the results a quick “success”, while preventing them from digging deeper to circumvent my defences.

I didn’t record the rejected voting attempts sadly, so I don’t know the true number of people just sitting there, thinking they had out-smarted the stupid guy that set up the site.

Graph comparing unique votes vs. unfiltered votes over time.

The graph shows that some people managed to vote multiple times, either by clearing their cookies or by switching to incognito. These attempts only represented a tiny fraction of the votes.

I know this, because after I started seeing people actually trying to vote twice, I wanted to prevent more knowledgeable attempts. In addition to the token, I also started storing a fingerprint and the voter’s IP server-side, along with their request. I now know that this is very wrong, but at the time I sadly wasn’t aware (The data is long deleted, was only ever used in order to provide these simple stats and never shared with anyone).

On one of the final days that the site was live (the 11th of May), you can see an explosion in votes, but the unique count tells a different story. About two thirds of the votes were cast from the same device. More incriminating is the absence of a user token for each of the votes, something which couldn’t have happened, had the public front-end been correctly used.

This targeted campaign, rating the meal with 1/5, was ran against the truly worthy target of the “Topfenschmarn”, a sweet pancake made from curd, enjoyed with berries (see this site for an example). Usually this is a tasty delicacy, but our canteen indeed often butchered it.

I forgive this vigilante for his methods because of his noble intentions - saving many pupils from this dish. It is also very funny to imagine someone clearing their browser’s cookies 27 times just to tarnish the reputation of a pancake.

Analysing the Votes

So what are the most liked meals our school canteen offered? The most popular meal, with a perfect score were chicken wings - perhaps unsurprisingly for a school canteen.

If you look at the amount of green (vegetables) in the most popular and then the most unpopular meals, it doesn’t take a data scientist nor a sophisticated model to spot a trend.

Meal (translated) Avg. Score
Chicken Wings 5.0
Pizza with salami 4.5
Ribs 4.5
Stir-fried vegetables and rice 2.5
Pork medallion with barley 2.0
Winter vegetable stir-fry 1.7

The data indeed confirms that vegetarian meals were rated, on average, almost an entire point lower than meals containing meat in some form or another, but mostly grilled or fried.

Vegetarian Average Score
No 3.80335
Yes 2.89559

Sometimes you don’t discover new things when doing data analysis, sometimes you only confirm what you already knew. But it’s nice when intuition and the facts line up - even though it came at the expense of more healthy and balanced meals at school…