Threat modelling for small apps
A lightweight way for small teams to threat model an app in an afternoon: assets, entry points, trust boundaries, a quick pass with STRIDE and a one-page template you can reuse.
Threat modelling has a reputation for being something large companies do with a security team, a week of workshops and a diagramming tool nobody enjoys. For small apps it can be much simpler. Threat modelling for a small team is an afternoon, a whiteboard and one page of notes, and it catches the kind of mistakes that turn into embarrassing security advisories later.
We build desktop and mobile apps and a few security tools, and we do a short threat model before any feature that opens a port, parses files from strangers or updates itself. This is the version we use. It is deliberately small. A threat model that takes a week does not get updated, and one that is out of date is only slightly better than none.
What a threat model is for
A threat model answers four questions, a framing popularised by Adam Shostack:
- What are we building?
- What can go wrong?
- What are we going to do about it?
- Did we do a good enough job?
That is the whole method. Everything else, including STRIDE, attack trees and data-flow diagrams, is a tool for answering one of those questions more thoroughly. For a small app you need just enough structure that you do not forget the obvious, and no more.
The output is not a document for an auditor. It is a short list of specific things to change in the code, plus a record of the risks you decided to accept, and why. The second list matters. Six months later, when someone asks why the local server does not use TLS, the answer should be written down.
Start with a drawing
Draw the app as boxes and arrows. You need four kinds of thing:
- Processes: your app, any helper processes, a background service.
- Data stores: settings files, a database, the keychain, a cache folder.
- External entities: the user, an update server, other devices on the network, a web browser, files that came from somewhere else.
- Data flows: arrows between them, labelled with what moves along each one.
Then draw the trust boundaries as dashed lines. A trust boundary is anywhere data crosses from something you control to something you do not, or between two things that run with different privileges. The network is a boundary. So is the edge of the user's account, the line between your app and the browser, and the line between your parser and the file it is reading.
The drawing at the top of this post is a typical small desktop app. It downloads updates from a server, runs a local HTTP server so other devices on the same network can reach something, stores settings and a key on disk, and opens files that could have come from anywhere. Five arrows cross a boundary. Those five arrows are where almost all of the risk lives.
That observation is the main shortcut for small teams. You do not need to analyse every box. Go to every arrow that crosses a dashed line and ask what could go wrong there.
A quick pass with STRIDE
STRIDE is a checklist of six kinds of threat, originally from Microsoft. It is useful because it stops you from only thinking about the attack you saw in the news last week.
| Spoofing | Someone pretends to be a user, a device or a server they are not. |
|---|---|
| Tampering | Data or code is changed in transit or at rest. |
| Repudiation | Someone does something and there is no way to show it happened. |
| Information disclosure | Data reaches someone who should not see it. |
| Denial of service | The app is crashed, hung or made unusable. |
| Elevation of privilege | Someone gets to do more than they should, such as run code. |
For each boundary-crossing arrow, go down the list and spend a minute on each letter. Most combinations will be "not relevant", and that is fine. You are looking for the two or three that make you pause. Write those down with a sentence each.
For small desktop apps, three areas come up so often that we check them every time.
Three things desktop apps get wrong
Local servers that listen to everyone
A lot of desktop apps run an HTTP server on the machine: for a companion web page, a browser extension, a preview, or to share files on a local network. Our own tool serve listens on 0.0.0.0:8080 on purpose, because its job is getting a folder onto a phone on the same network, and it offers --auth and --tls for exactly the reasons below. The mistakes here are well known and still common.
The first is binding to 0.0.0.0 when you meant 127.0.0.1. The first listens on every network interface, including the coffee shop Wi-Fi. If the server is only meant for the same machine, bind to loopback. If it is meant for the local network, make that a visible, deliberate choice for the user, and say which folder is being shared.
The second is assuming that only your own code talks to a loopback server. Any web page open in the user's browser can send requests to localhost. Browsers are adding restrictions on public sites reaching local addresses, but you should not depend on them. Check the Host header to block DNS rebinding, check Origin on anything that changes state, and require a random token for anything that is not public. Do not add a permissive CORS header to make an error go away.
The third is path traversal. If a request for /files/../../.ssh/id_ed25519 returns the key, the sharing feature has become a leak. Resolve the full path and confirm it is still inside the shared folder before opening anything.
File parsing
Any app that opens files a user downloaded is parsing input written by strangers. Images, archives, documents and metadata formats have long histories of parser bugs that lead to crashes at best and code execution at worst. That puts denial of service and elevation of privilege on the table for a feature that looks harmless.
The defences are unglamorous. Use well-maintained libraries rather than writing your own parser for a complex format. Prefer memory-safe languages for parsing code where you can. Put limits on sizes, nesting depth and decompression ratios, because a small zip file can expand to fill a disk. Fuzz the parsing code if you do write it. On macOS, the App Sandbox limits what a compromised process can reach, and it is worth the setup effort for anything that opens untrusted files.
Updates
Auto-update is the most powerful feature in most desktop apps, because it is a channel that installs code on the user's machine. If an attacker can tamper with an update, they own every user at once.
Serve update metadata and downloads over HTTPS, and verify a signature on the downloaded file with a key that is not stored on the same server. On macOS, the Sparkle framework handles this with EdDSA signatures, and apps should be signed and notarised. On Windows, sign installers with Authenticode and check the signature before running them. Keep the signing key offline or in a hardware token, not in a CI variable that every contributor can print.
A one-page template
This is the page we fill in. It fits on one screen, lives next to the code, and gets updated when a boundary changes.
THREAT MODEL: <feature or app> DATE / AUTHOR
1. WHAT WE ARE BUILDING
One paragraph. Link to the diagram.
2. ASSETS
What would hurt if stolen, changed or lost?
(user files, API keys, signing key, reputation)
3. ENTRY POINTS
Every way data gets in: ports, files, URLs,
IPC, updates, clipboard, drag and drop.
4. TRUST BOUNDARIES
List each dashed line on the diagram.
5. THREATS (one line each, STRIDE letter first)
T update download replaced in transit
I local server serves files outside share
...
6. DECISIONS
Fix now / fix later (with issue link) / accept
and why.
7. REVIEW WHEN
New port, new file format, new third-party
service, new privilege.
Assets deserve a moment of thought, because they decide what "bad" means. For a file-sharing feature, the asset is the rest of the disk. For a password manager, it is obvious. For a small utility with no network access and no secrets, the honest answer may be that the main asset is the user's trust, and the main threat is a malicious update.
Keeping it small
The failure mode of threat modelling is not doing it badly. It is doing it once, thoroughly, and then never again. We keep ours short so that updating it is a ten-minute job during code review. A pull request that adds a port, a new file type or a new external service should touch the threat model file in the same change. If it does not, the reviewer asks why.
If you have never done one, pick the app you are working on now, draw it on paper, and mark the dashed lines. Count the arrows that cross them. That number is your list for the afternoon, and if you would like a second pair of eyes on it, get in touch.