Every guide in this series so far has modelled systems you operate. The servers are yours, the network is yours, the runtime is yours, and the attacker has to get in. A mobile app inverts that completely: your code runs on hardware belonging to the person you are defending against, in a process they can inspect, on an operating system they can modify, with unlimited time and no rate limit. There is no getting in, because they started inside.
That single fact reorganises the whole model. Questions that are meaningful for a web backend — can an attacker read this value, can they change this logic, can they see this key — have known answers on mobile, and the answer is always yes. So the useful questions are different ones: what does it cost you when they do, what does the server do about it, and what should never have been on the device in the first place. A mobile threat model that produces a list of things to hide in the binary has answered the wrong question. One that produces a list of decisions about what the device is trusted to assert has answered the right one.
The short version: Model three zones — device, transport, backend — and grant the first one no trust at all. Assume a rooted handset running a repackaged build under instrumentation, because that is a weekend, not a nation state. Then work three inventories: what the app leaves at rest, what it exposes to other apps on the same phone, and which rules it enforces that the server does not. The last of those is where the exploitable findings are; the first is where the mass-compromise findings are.
Three zones, not one diagram
The temptation is to draw the app as a single process talking to an API and move on, which collapses three very different trust situations into one box. Split it, and give each zone its own trust boundary on the data flow diagram:
| Zone | What lives there | How much you can trust it |
|---|---|---|
| Device | The binary, local storage, caches and logs, the keychain or keystore, WebViews, embedded SDKs, other apps on the same handset | None. Everything here is readable and modifiable by a determined owner of the device. |
| Transport | TLS sessions to your API, to third-party SDK endpoints, and to any CDN or media host | Solid against the network, weak against the device — the user can always see their own traffic. |
| Backend | Your API, auth service, and data stores | The only zone where a control is a control. Everything the model actually relies on has to be enforced here. |
Drawing it this way makes the central asymmetry visible to the room immediately, and it also clarifies scope: the backend half is an API threat model and should be done as one, following the endpoint-and-object axis rather than being re-derived here. What this post covers is everything on the phone side of that boundary, which the API model does not reach.
Start from a hostile device
Agree the attacker's capabilities out loud before modelling anything, because otherwise half the session is spent arguing about whether each individual threat is realistic. The baseline is not exotic. Assume someone can obtain your app package, decompile it and read the resulting code, run it on a rooted or jailbroken handset, attach a debugger, hook and rewrite functions at runtime with off-the-shelf instrumentation frameworks, intercept and modify their own TLS traffic by installing their own certificate authority, repackage the app with modifications and re-sign it, and automate all of the above across an emulator farm.
Every one of those is a documented, tooled, tutorialised procedure. None requires a vulnerability in your app or the platform. Taken together they mean the same thing the API guide said about user interfaces, only more so: the client is a suggestion. It is a convenient way for honest users to interact with your system, and it is a starting point for everyone else.
Worth naming separately, though, is that this baseline attacker is usually attacking their own account. That distinction matters when you prioritise. A user unlocking a paid feature on their own handset is a revenue problem. A user extracting an API key that works for every account, or discovering that the server accepts a balance the client calculated, is a breach. Both come out of the same session; only one belongs at the top of the risk register.
What the app leaves on the device
This is the inventory that produces findings affecting every user at once, because losing a phone, restoring a backup to a new handset, or handing a device to a repair shop are ordinary events, not attacks. Walk the list and, for each item, ask what an attacker gains from reading it and what the user loses.
| Where data ends up | What to ask |
|---|---|
| Keychain / Keystore | The right place for secrets — but with which accessibility and protection attributes? Is the item available when the device is locked, is it restored to a new device from backup, is hardware backing actually being used or silently falling back? |
| Preference files and plists | The wrong place for secrets, and where tokens routinely end up because the API is convenient. Anything here is plain text to a file browser. |
| Local databases and caches | Offline-first apps hold a copy of everything the user has ever viewed. Is it encrypted, is the key derived from something the device holds, and does logout actually delete it? |
| Logs and crash reports | Debug logging that survived into release, and crash payloads that carry request bodies, tokens, or personal data to a third party. |
| Screenshots and the app switcher | The OS captures the screen when the app backgrounds. If that screen shows an account number or a message, the image is now on disk. |
| Clipboard | Copied one-time codes and card numbers are readable by other apps, and may sync across the user's other devices. |
| Cloud and local backups | Which app data is included in device backups, and is the security of that data now the security of the user's cloud account password? |
| Third-party SDK storage | Analytics and ad SDKs keep their own caches and identifiers inside your sandbox, on their schedule, under their rules. |
The strongest finding this inventory produces is usually not "X is stored insecurely" but "X does not need to be here at all." Data that never reaches the device cannot be extracted from it, and the fix — fetch it on demand, hold it in memory, scope the token narrowly, shorten its life — is generally cheaper than defending it in place.
The other apps on the phone
A phone is a multi-tenant environment where the other tenants were installed by the user and may be hostile. Your app's inter-app surface is an API in every meaningful sense, and it is almost never modelled as one.
- Deep links and custom URL schemes. Any app can invoke them, with any parameters. A link that opens a screen is fine; one that carries an action, an identifier, or a token is an unauthenticated endpoint whose caller you cannot verify. Custom schemes in particular can be claimed by another installed app.
- Exported components and providers. Anything the manifest exposes can be called by any other app on the device. The default is frequently more open than intended, and a content provider exporting the local database is the classic full compromise.
- Extensions, share targets, and widgets. Additional processes with their own entry points, often sharing a data container with the main app and receiving far less review.
- Notification payloads. Content visible on a lock screen is content visible to anyone holding the phone. Rich payloads may also be processed before the user authenticates.
- WebViews and JavaScript bridges. The highest-severity item on this list. A bridge that exposes native functions to page JavaScript turns any content injected into that WebView — a compromised partner page, an unvalidated redirect, an ad frame — into native code execution inside your app, with your permissions and your storage.
Treat each of these the way the API guide treats an endpoint: who can call it, what may they do with it, what does it return. The answer to the first is always "any app the user installed," which makes the other two questions considerably more interesting.
Client-side controls are UX, not security
This is the section that changes the most minds, so it is worth doing carefully rather than dismissively. Client-side protections are not worthless — they raise cost, they filter out casual tampering, they are sometimes required by a scheme or a regulator, and they buy detection signal. What they cannot do is enforce anything, because they execute in a context the attacker controls. The rule is simple: every one of them needs a server-side counterpart, and the model should record what that counterpart is.
| Client-side control | What it is actually worth | What the server must do |
|---|---|---|
| Root / jailbreak detection | Deters casual users; bypassed with a hooking script by anyone who has read a tutorial. | Treat the signal as risk input, not as a gate. Never assume an unflagged client is clean. |
| Certificate pinning | Genuinely effective against network attackers; irrelevant against the device owner, who can patch the check out. | Keep it — it defeats a real threat — but do not treat "traffic reached us over a pinned connection" as authentication. |
| Obfuscation | Buys time against reverse engineering. Time, not secrecy. | Assume the logic is known. Nothing in the binary is a secret, including anything you encrypted with a key that is also in the binary. |
| Client-side validation | Good UX, zero security value. | Re-validate everything, including fields the app makes it impossible to enter incorrectly. |
| Hidden or flagged features | Invisible in the interface, present in the binary, discoverable by anyone reading it. | Enforce entitlement server-side on every request, not by hiding the button. |
| Biometric unlock | Strong when it gates a key held in secure hardware; theatre when it gates a boolean in your own code. | Bind the authentication to a key the platform releases only on success, so bypassing the check yields nothing usable. |
The practical exercise is quick and uncomfortable: list every rule the app enforces — prices, limits, entitlements, sequence, eligibility, quantities — and for each one name the server-side check that enforces it again. The ones with no answer are the findings, and they are usually the highest-severity items in the whole model, because they are exploitable by a single user with a proxy and no special access.
The STRIDE sweep, at mobile altitude
| Category | What to ask of a mobile app |
|---|---|
| Spoofing | What proves a request came from your app rather than a script? Platform attestation raises the bar; a hardcoded key or a custom header does not. Can a repackaged build authenticate normally — and should the server care? |
| Tampering | What can be modified: the binary, the local database, the traffic, the responses the app trusts? What does the app do if a response is altered — does a modified entitlement flag unlock anything the server does not re-check? |
| Repudiation | Are actions logged server-side with device and session context, or does the audit trail depend on the client reporting honestly about itself? |
| Information disclosure | What is in the binary (endpoints, keys, staging hosts, comments), on disk, in logs, in the app switcher snapshot, in notifications, and in the payloads sent to embedded SDKs? |
| Denial of service | Can a scripted client exhaust an expensive endpoint, burn a user's quota, or trigger costly server work at machine speed? Is there a path to disable a broken release without waiting for store review? |
| Elevation of privilege | Can a deep link or exported component reach functionality the interface gates behind authentication? Does a WebView bridge expose native capability to remote content? Can one user's device act for another account? |
The versions you cannot retire
A web application has one live version; a mobile app has every version you have ever shipped, running on handsets you cannot reach, indefinitely. Users on old operating systems cannot upgrade, users with automatic updates disabled will not, and store review sits between you and any fix. This changes three things in the model, and they are easy to miss because nothing on the diagram represents them.
First, your API's real contract is with every app version still in the field, so a server-side authorization rule added last quarter has to hold for a client built two years ago that knows nothing about it. Second, a vulnerability in a shipped release cannot be patched in place — the remediation is a new release plus adoption time, which is measured in weeks, so a kill switch or forced-upgrade path is a genuine control worth building before you need it. Third, credentials embedded in a shipped binary can never be rotated cleanly, which is the most concrete argument available for why they should not be there. Record the minimum supported version as an explicit assumption in the model, because it silently defines the weakest client your backend must defend against.
The SDKs in your process
Mobile is the harshest environment for third-party integrations, because an embedded SDK is not a service you call — it is code running inside your process, holding your permissions, reading your sandbox, seeing your screens, and shipping on the vendor's release schedule. There is no boundary between it and you other than the one the platform enforces on your app as a whole. Ask which SDKs are present, which are still there because they were needed once, what each one transmits and to whom, whether an SDK update is reviewed or automatic, and whether the app store privacy disclosures still match what they actually collect — because those disclosures are your declaration, not the vendor's.
What to record
Split findings by who is affected. Threats where the attacker is the device owner acting on their own account cap out at fraud and revenue loss; threats where compromising one device yields something reusable — a shared key, a server-side gap, a token that works elsewhere — scale to every user, and the register should make that distinction visible rather than leaving it to a severity number to imply. Where the fix is systemic, record it once: "no server-side re-validation of client-supplied prices" is one architectural entry, not one per screen.
Then write the assumptions down, because mobile assumptions are unusually load-bearing: the minimum OS and app version you support, which data you believe never touches disk, which SDKs you believe are present, which platform protections you believe are active. The natural triggers for revisiting are a new SDK, a new deep link, a new WebView, and a platform release — the last of which can change the security behaviour underneath your app without a single commit on your side. If you want a checklist to grade the result against, the OWASP Mobile Application Security project maintains a verification standard and testing guide that map closely onto this ground.
Where mobile threat models go wrong
- Modelling the app and skipping the backend. The app is the part you can see; the backend is where the controls are. Both need modelling, and they need it on different axes.
- Trying to protect secrets in the binary. There is no way to store a secret on a device the attacker controls. The fix is architectural: do not need one there.
- Counting client-side defences as controls. Pinning, obfuscation and root detection all raise cost. None of them enforces a rule.
- Ignoring the inter-app surface. Deep links and exported components are unauthenticated entry points, and they get a fraction of the review a network endpoint gets.
- Assuming everyone upgrades. Your oldest supported client defines your real attack surface, and it is older than you think.
- Treating an SDK as a library. It is a third party executing in your process with your permissions.
For a mobile app that carries money or health data, it is worth taking one goal — "take over another user's account from my own handset" — and building the attack tree beneath it. The branches are the ones this post has walked: something extracted from the device, something exposed to another app, something the client was trusted to assert. What the tree shows is which branches terminate at a server-side check and which terminate at nothing, and that is the picture worth putting in front of whoever decides what gets fixed before the next release.
Mobile banking and payment apps carry the strictest version of this, with scheme rules and regulatory expectations attached to the device-trust decisions in particular. See threat modeling for fintech, or start from the pre-labelled DFD in the free threat modeling templates.