Start a project

The threat model hasn’t changed, the defaults have

The original version of this article walked through a man-in-the-middle attack in detail: an attacker on the same coffee-shop network poisons the ARP table, intercepts traffic between an app and its API, and reads whatever isn’t encrypted. That attack is exactly as viable today as it was then, on an open network with no protections. What’s different is that the industry stopped leaving this to individual developers to get right.

TLS by default is no longer optional or a decision

A decade ago, an app sending plain HTTP “because the data isn’t sensitive” was a judgment call an individual developer made, and plenty made it wrong. That judgment call has since been taken away. Both major mobile platforms now default to blocking cleartext traffic:

  • iOS App Transport Security has required HTTPS by default since iOS 9, and getting an exception approved by App Store review for a production app is difficult enough that it’s not a practical way to ship plaintext traffic.
  • Android’s Network Security Config blocks cleartext traffic by default for apps targeting API level 28 and above, and an explicit usesCleartextTraffic="true" declaration is required — and visible in the manifest — to override it.

The practical effect: “did we remember to use HTTPS” isn’t really a live question for a modern mobile app anymore, because the platform won’t let the app talk to a plain-HTTP endpoint without a deliberate, visible override. The remaining question is what happens inside that TLS connection.

Certificate pinning: real protection, real cost

Certificate pinning — hardcoding which certificate (or public key) the app will accept for a given host, instead of trusting anything signed by a certificate authority the OS trusts — defeats a specific attack TLS alone doesn’t: a compromised or coerced certificate authority issuing a valid- looking certificate for your domain to someone who isn’t you. Without pinning, the app has no way to tell the difference between your real certificate and a fraudulent one that a trusted CA issued in error or under duress.

The cost is operational, and it’s a real one: when you rotate your own TLS certificate — routine, should happen regularly — a pinned app that wasn’t updated in advance can’t connect to your API at all until it gets an update through app store review. This has caused real outages. The practical middle ground most teams land on:

  • Pin to a small set of certificates (current and next, so a planned rotation doesn’t break old app versions), not a single certificate.
  • Pin to a backup CA’s public key as a fallback path, not just your primary provider’s, so a CA-level problem doesn’t strand every existing app install.
  • Reserve pinning for apps handling genuinely high-value data — banking, health records, anything with regulatory weight — where the operational cost is clearly worth the attack it prevents. A general-purpose consumer app usually isn’t in that category, and unpinned TLS plus the platform defaults above is a reasonable, much lower-maintenance choice for it.

Secrets never belong in the client, full stop

Anything shipped inside an app binary — an API key, a signing secret, a hardcoded credential — is not actually secret. Decompiling an APK or inspecting an app bundle to extract a string constant is routine, low- effort work, and no amount of obfuscation changes that a string embedded in a distributed binary can be recovered from it. This was true in 2015 and it is unchanged now; what’s changed is that it’s much better understood as a baseline expectation rather than an edge case worth mentioning.

The fix isn’t hiding the secret better, it’s not needing one in the client in the first place:

  • A backend-for-frontend or a thin proxy holds the actual third-party API key server-side; the client only ever talks to your own backend, which makes the calls that need the real secret.
  • Anything the client genuinely needs to authenticate itself with should be a user token, scoped to that user’s own access and revocable independently — never a shared API key that every install of the app carries identically, because a single leaked copy compromises every user, not just one.

API keys vs. user tokens is the distinction that matters

The original article treated “credentials the app sends” as one category. It’s worth being precise about two different things that get lumped together:

  • An API key typically identifies an application or a project, not a person, and is meant for server-to-server calls where the caller can keep it confidential — it should essentially never ship inside a distributed client binary.
  • A user token (see the companion piece on token authentication) identifies a specific person’s session, is short-lived, and is exactly what a client app should be sending on the user’s behalf.

Confusing the two — embedding an API key in the app to “authenticate the app” — is precisely the mistake that turns a client-side secret leak into an account-wide or fleet-wide compromise instead of a single user’s problem.

The version of “safe connections” that holds up

Platform defaults now do most of what used to require deliberate care: TLS is the default transport, and cleartext requires an explicit, reviewable opt-out. What’s left to get right by design, not by platform enforcement, is choosing pinning deliberately for the data that warrants its operational cost, and never putting a secret that would compromise more than one user into a binary anyone can decompile.


Originally published in 2015 and updated for 2026.

security · mobile · apis

30 minutes with a senior engineer.

Tell us what you're building. You'll leave with an honest opinion, even if it's "you don't need us."

Reference calls with past clients are available under NDA during evaluation.