Trusting clients is probably a security flaw

If your service needs to trust the clients, hold my Big Mac

A photo of a car trunk, full of McDonald's paper bags and drinks, a McDonald's by-road sign covered by some flame, covered by a caption: "today's food was paid for by a lack of security principles"

I looked at a discussion on blink-dev Google Group and saw the message:

Perhaps it is a good thing for user choice to have a browser that is fully open to any use and allows anonymous user actions.

The result of such open-ness is that an entire series of services that need to trust the client (used in the oauth sense of the word) are not available to web apps. […]

I have recently worked on a fork of Chromium that is designed to have this functionality and on Native Wallet apps to get it. The lack of this functionality in Chrome will drive developers away from Chrome and fragment the user experience. We already have the problem of directing users away from Chrome to a secure wallet and being unable to bring the original user session back to Chrome. Of course Google and Apple get to solve this problem with their own wallets, but that will not fly in Europe and now the US DHS is asking for solutions that are more open as well.

My understanding of security started an internal screaming.

Meet the McDonald's app

Hold on, I'm not at all joking. The McDonald's app developers put a lot of effort into policing the clients allowed to even dream about running the app. The app was checking for:

  • whether you have a directory called TWRP in your internal storage. TWRP is a custom recovery, and by default, stores device backups you generate from recovery in there. You have a device backup? Then no deals for you, nerd.
  • whether you have the com.topjohnwu.magisk app installed. You like modifying your devices, don't you? Pay full price, nerd.
  • whether you have installed the app from, more widely known as Google Play. What, you don't have it? That'll be €14,50, nerd.
  • check your device with RootBeer, a library that tries to check whether you have root access. Not passing? Card or cash, nerd?
  • and finally, using the SafetyNet API (now being rebranded into Play Integrity, specifically the Device Integrity and App Integrity parts, with some minor changes). Not passing? Pay. Nerd.

This is more annoying than any financial app I've had, and I have 5 of them on my phone. uses Play Integrity API, but that's to use contactless payments via BLIK, and the rest of the app works. IKO, the app of PKO BP, seeing that I have root access, disabled logging in with biometry, requiring me to log in with a PIN code. bunq told me that maybe my device should not be rooted. The worst one seems to be Revolut, which blocked my access, clearly stating the reason to be root access.

The McDonald's app only displays a generic error message, and an error code that tells you nothing.

And what are guarding so much?

What are the Deals inside the app? Currently, I can get a free burger for 650 points, that is, if I spend 65 Euro in McDonald's first. What a deal. But that's not what brings me here.

There once was a promotion in collaboration between Coca-Cola and McDonald's, specifically available in Poland. It boiled down to this:

  1. Buy 3 bottles of Coca-Cola.
  2. Enter 3 codes (from each bottle) into the Coca-Cola app.
  3. Enter the code you got from the Coca-Cola app into the McDonald's app.
  4. In return, you get a free meal with a Big Mac.

Ignore this unnecessary duplication with copying the code between 2 apps. Actually, ignore the Coca-Cola part as a whole. So, what happens in the McDonald's app? As with many things in that app, this deal was operated by a page opened in a WebView. The config for the country was set to point to the deal's page from the main screen. On the page, there was a pretty simple form:

Screenshot of application interface, in Polish, with a text input and a confirm button, asking to enter a voucher code from Coca-Cola.

Obscurity is not security

From now on, this is basically a post mortem, except it's not McDonald's writing it.

The page opened in the WebView was sending the entered code in a request to a McDonald's server. But here's the flaw: the request checking and invalidating the code, was doing just that. It just returned whether the code is valid. The page was requesting a check whether the code was valid, and the client was assigning the coupon to the user.

It turns out that the company's servers do not verify this and take the app "for its word", reported

When you say you need to check whether the request is coming from a "trustworthy client", I say trust no client, use a rubber.

An untouched device can exploit your service, too

Oh, right, I said earlier about some measures? This whole exploit could (at least at that time) be executed from an unmodified device. Open the app, go to privacy policy (opens in a WebView), find an external link. Be creative. Find a link to Google's privacy policy, and tap their logo to go to Google Search. Find a link to YouTube, and search for a video with a link somewhere else. Whatever gets you to a page that can execute a little bit of JavaScript for you.

  loyaltyId: 2424,
  rewardId: 95275,

This was the thing.

But don't worry. If this is fixed, nothing is lost. You remember the measures I talked about? They indeed check signs that might indicate you're not trustworthy. But really, passing these checks might just mean you know how these checks work. What can and does go wrong?

You can just comply with the checks.

TWRP backups? Change the directory name.

Magisk Manager app? Change the package name in the settings.

Not installed from Google Play? Open terminal and run pm set-installer Or install with pm install -i [file]. Or the same with adb.

Root checks? Add McDonald's to Zygisk Denylist.

You can just not run the checks.

Oh, that was just the way where we obediently fulfil these checks. But if this runs on your device, you are the one in control of it. You can inject into the process with tools like Zygisk or LSPosed, and remove these checks. I have injected into 2137 processes on my phone today. Nobody controls this.

You can just tell the checks what they want to hear.

And the last check. SafetyNet/Play Integrity. Check whether you pass with SPIC. If you don't pass basic SafetyNet, install MagiskHide Props Config, run props in console, change signature to something from the available list. Now, if you don't pass CTS SafetyNet, install Universal SafetyNet Fix. Yes, my phone really just passes SafetyNet and Play Integrity like this (Integrity up to MEETS_DEVICE_INTEGRITY, without MEETS_STRONG_INTEGRITY).

No labels (a blank value): The app is running on a device that has signs of attack (such as API hooking) or system compromise (such as being rooted), or the app is not running on a physical device (such as an emulator that does not pass Google Play integrity checks).

This is a fragment of the Play Integrity documentation. But the problem with checking if the user is a god, is that the user is a god. They can just tell you what you want to hear.

(On a side note, if you speak Polish, I recommend reading "Aplikacja mobilna nie wie, czy telefon jest zhackowany" from Informatyk Zakładowy)

But typical users will not study your app if it doesn't work

Mass exploitation was, in the end, stopped by ending the deal, because of how out of control it was. So who was stopped?

  • Unaware users. Every time you introduce a stupid check, there is a user with a false positive result. For example, some of the RootBeer checks will trigger on some unmodified Xiaomi, Asus, or Fairphone, or random cheap phones that happened to be in someone's nearest Tesco. The biggest irony is that some of these users will root their phones to bypass these checks instead.

    If your app starts looking for the MEETS_STRONG_INTEGRITY label from Play Integrity, even more users will be affected like this.

    Literally look at the Google Play reviews of the McDonald's app. This app has a 2,8/5 rating and you have to keep scrolling them to find positive ones, from the very few users who managed to get the app running.

  • Users with root access to their own phones. First of all, why? What's bad about having administrator access to your own phone?

    Second, this is ineffective if they know how to workaround. They can inject into your process and tell you it's a good phone ma'am.

  • Huawei users. Or Amazon Fire users. Or any Android device made for China market users. Or users of a mobile Linux distribution, trying to run an app over Waydroid. Their devices by default do not have Google Play Services that can be tricked like this, and will require more work than I described to pass these checks.

But will it at least stop fraud?

Hell no.

A photo from a bot farm with lots of phones pinned to a wall, with USB cables connected to them.

I'm currently unemployed. if you are looking for someone experienced with Rust, TypeScript, as an SRE, and/or in Linux software distribution (Electron, Flutter), please contact me.

or - while I'm not struggling to stay alive - if you like my work, please consider sending me a small donation.

GitHub Sponsors, ko-fi, bunq: EUR, USD, GBP, PLN.

some cooler entities to also read. generated with frenring.