All posts Backend

Never Trust the Client: The Foundation of Mobile App Security

A mobile app is a program that runs on the user's device and can be inspected there. That leads to a single rule: nothing coming from the client can be taken as true. Here is what that rule means in practice.

DT
Demir Taşdemir Mobile App & Web Developer
— min read

A mobile app is a program that runs on the user's own device. The user can open that file, look inside it, watch its network traffic while it runs, and if they want, modify it and repackage it. That is not a vulnerability — it is the nature of the medium.

A single rule follows from this, and all of mobile security practice is really just the application of that rule: no information coming from the client can be taken as true. In this post I have written down what that rule means concretely, along with the violations I see most often.

1. Hardcoding a secret key into the app

This is the most common mistake. Writing a service key into the code as a constant does not hide it — it only makes finding it a few minutes harder. Extracting the text strings from a compiled app is step one.

A distinction is needed here:

  • Keys that are fine to be public — some services design their client keys this way; keeping them in the app is normal, but usage restrictions (bundle identifier, signature, domain, quota) must always be defined.
  • Keys that must stay secret — a payment provider's secret key, database admin access, an email sending key. These must never, under any circumstances, live in the app.

Every operation in the second group must happen on the server, not in the app. The app tells the server “do this”; the party that uses the secret key is the server. It is a one-line rule, but it shapes the architecture from the start.

Obfuscation is not security

Obfuscation and minification tools make code harder to read and reduce app size — both useful. But they do not protect a secret. A key can still be extracted from an obfuscated app; it just takes a little longer. Use obfuscation as one layer, not as your only defense.

2. Validating only on the client

Form validation happens on the client for the sake of user experience — so the user sees their mistake without a round trip to the server. But validation done for security has to live on the server, because the client side can be bypassed.

A concrete example: on a listing creation screen, the price has to be greater than zero. You check it on the client, and someone sending a request straight to the server can create a listing priced at zero. The same check has to be performed on the server too — except it is not a repeat; that is where the real check belongs.

The test I apply is this: if this check is skipped, does data get corrupted, does money get lost, does someone reach another person's data? If the answer is yes, the check belongs on the server. If no, it can stay on the client.

3. Taking the price and quantity from the client

This is the most expensive special case of the previous point. Sending the product's price inside the order request means letting the user set the price.

The correct flow is this: the client sends only what it wants — the product identifier and the quantity. Price, discount, shipping and total are calculated on the server:

// What the client sends
{ "urunId": "p_4471", "adet": 2, "kuponKodu": "YAZ25" }

// What the server does
// 1. Read the product from the database, take the current price
// 2. Check whether there is enough stock
// 3. Check whether the coupon is valid, applies to this user, and has not expired
// 4. Calculate the total
// 5. Start the payment based on this total

The same logic applies to in-app purchases. Whether a user has a subscription is not decided by the app saying “purchase succeeded.” The receipt that comes from the store is checked server-side against the store's validation service, and the user's entitlement is determined by the record on the server.

4. Confusing authentication with authorization

Authentication answers the question “who are you.” Authorization answers “do you have the right to do this.” The second one gets skipped far more often.

The classic mistake: accepting a request because the user is signed in. But a signed-in user can change the identifier inside the request and try to reach someone else's data. On every request the server has to ask: is the user in this session the owner of this specific record?

Reading the user identity from the verified session rather than from the request solves this problem at the root. I never trust a kullaniciId field in the request body.

Actually write your database security rules

If you use a managed database, the rules file is often the most critical security layer — because the client talks to the database directly. Rules left “open to everyone” during development must be tightened before release. I review this file as carefully as the app itself, and I write read/write permissions separately for every collection.

5. Storing sensitive data in the wrong place on the device

Things like session tokens and refresh tokens should not sit in plain preference storage but in the secure storage the platform provides — the keychain on iOS, encrypted storage on Android. Plain preference files can be read on a device with root access.

On top of that: not storing sensitive data unnecessarily is a security measure in itself. Every piece of data you keep is a liability you have to protect. Never collecting credit card details is always better than storing them safely — which is why redirecting to the payment provider's own interface is both secure and cheap.

6. Logging everything

While debugging, printing entire requests and responses to the log becomes a habit. If that code survives into the release build, tokens and personal data end up in device logs.

What I do is disable logging calls entirely in the release build, and mask the places that print sensitive fields from the start. That is why my pre-release checklist has an item that reads “are debug logs off.”

7. Leaving network traffic unprotected

Today every request goes over an encrypted connection; that is the default by now. What you can add on top is certificate pinning: making the app trust only a specific certificate.

This is a serious layer against man-in-the-middle attacks, but it has a cost: when the certificate is renewed, the app has to be updated too, otherwise none of the users in the field can connect. So I use pinning not in every project, but in projects that genuinely have high-risk flows, and always with a backup certificate defined.

A checklist

The security list I go through before releasing an app:

  • Is there a key inside the app that should have stayed secret
  • Do the public keys have usage restrictions defined
  • Does every write operation pass an authorization check on the server
  • Are amounts and prices calculated on the server
  • Are purchases validated on the server
  • Have the database security rules been tightened
  • Are tokens in secure storage
  • Are debug logs off in the release build
  • Does the data the app collects match the store declaration

None of the items on this list are complicated. They are all different views of the single sentence I wrote at the beginning: the client is not under your control, so every decision that matters has to be made on the server.

  • Backend
  • Security
  • Mobile Development
  • Architecture
Share: LinkedIn X WhatsApp
DT

Demir Taşdemir

Mobile App & Web Developer

I have been building software since 2018. I have published 11 apps on the App Store and Google Play; right now I am working on 6 mobile apps, 1 e-commerce platform and 1 desktop game.

Does your app's security need a review?

I can go over your current setup for authorization rules, key management and server-side validation. Just write to me from the contact page.