Threat Modeling for Engineers — A Practical Guide

A scanner will tell you that a dependency is three versions behind and that you forgot a security header. It will not tell you that your password-reset endpoint trusts a user-supplied email field to decide whose account to reset. That is a design flaw, and no amount of SAST, DAST, or dependency scanning catches it, because the code does exactly what it was written to do. The bug is in the shape of the system, not the syntax. Threat modeling is how you find those flaws before they ship.

What it actually is #

Threat modeling is a structured conversation about how your system could be attacked, held while the design is still cheap to change. It is not a compliance artifact and it is not a tool you buy. At its core it is four questions, popularized by Adam Shostack, that a team walks through together:

What are we building? Draw the system so everyone is looking at the same picture.

What can go wrong? Enumerate threats against that picture.

What are we going to do about it? Decide on mitigations for the ones that matter.

Did we do a good job? Check that the model matches reality and the mitigations landed.

Most of the value lives in questions one and two. If you only ever draw the diagram and argue about what could go wrong, you are already ahead of most teams.

Draw the data-flow diagram #

You cannot reason about attacks on a system you cannot see. A data-flow diagram (DFD) is the cheapest way to make the system visible. You need exactly four kinds of thing:

External entities: users, third-party APIs, anything outside your control (drawn as boxes).

Processes: your code that does work, like the API service (circles).

Data stores: databases, queues, caches, object storage (parallel lines).

Data flows: arrows showing what moves where.

Then draw the part that makes it a security diagram: trust boundaries. A trust boundary is any line where the level of trust changes. The browser talking to your API is one. The API talking to the database is another. Data crossing a trust boundary is where attacks happen, so those crossings are exactly where you focus.

Here is a mini DFD for a sample app, described in text:

code

[Browser] --HTTPS--> ( Web/API service ) --SQL--> [ App database ]
| |
| trust boundary #1 | trust boundary #2
| (internet -> DMZ) | (app -> data tier)

Two boundaries, three components. That is enough structure to run STRIDE against.

STRIDE: a checklist for "what can go wrong" #

Staring at a diagram and asking "what could go wrong" leads to blank whiteboards. STRIDE gives you six prompts, each the inverse of a property you want. Walk each component and each data flow through the six letters. You will not find something for every cell, and that is fine.

STRIDE category | Property it violates | Example threat in our sample app | Mitigation → ticket
Spoofing | Authentication | An attacker calls the API with a forged or stolen session token and acts as another user. | Short-lived signed tokens, rotate secrets, bind sessions to device where feasible.
Tampering | Integrity | A client modifies the role=user field in a request to role=admin on the way in. | Server-side authorization checks, never trust client-supplied roles, validate input.
Repudiation | Non-repudiation | A user performs a destructive action and later denies it; there are no logs to prove otherwise. | Append-only audit log of security-relevant actions with actor, timestamp, and request ID.
Information disclosure | Confidentiality | A verbose error returns a SQL string and stack trace exposing schema and internal paths. | Generic error responses to clients, detailed errors only in internal logs, TLS in transit.
Denial of service | Availability | An unauthenticated endpoint runs an expensive query and is hammered until the DB falls over. | Rate limiting, query timeouts, pagination caps, autoscaling with a ceiling.
Elevation of privilege | Authorization | A regular user hits an admin-only route that only hides its link in the UI. | Enforce authorization at the API layer per route, deny by default, add integration tests.

Notice that every mitigation in the right-hand column is something you can hand to a developer. That is the point of the exercise: it converts vague unease into a backlog.

From threats to tickets #

A threat model that ends on a whiteboard photo is theater. For each threat you decide to act on, make a real decision and record it. You have four honest options for any threat: mitigate it (build the fix), accept it (document why the risk is tolerable), transfer it (offload to a provider or insurance), or eliminate it (remove the feature). Then file tickets for everything you chose to mitigate, with a link back to the model so the reviewer knows why the work exists.

Prioritize ruthlessly. The forged-token and privilege-escalation threats above ship this sprint. The exotic timing attack that requires local network access might get a comment and an accept decision. Threat modeling is about spending your finite security attention on the things most likely to hurt.

Keep it lightweight #

The fastest way to kill threat modeling in an organization is to make it a 40-page document that one person owns and nobody reads. Do the opposite. The right unit of work is a whiteboard session per feature: the two or three engineers who are building the thing, thirty to sixty minutes, a diagram, and a table like the one above dropped into the design doc or the pull request. If it takes longer than a code review, you are over-modeling.

When to do it, and with what #

Do it at design time, before the code exists, because that is when changing the shape of the system is nearly free. Then revisit whenever the architecture changes in a way that moves a trust boundary: a new external integration, a new data store, a new authentication path, or exposing something previously internal. Routine features that stay inside existing boundaries usually do not need a fresh model.

For tooling, do not overthink it. A shared doc with a table is a legitimate threat model. When you want a real diagram, OWASP Threat Dragon is free, open source, and does STRIDE suggestions per element. The Microsoft Threat Modeling Tool is solid if you live in that ecosystem. The tool never matters as much as the conversation. If pairing STRIDE with a concrete vulnerability catalog helps your team, walk the model against the OWASP Top 10 as a sanity check. And fold the output into your broader application security best practices so the fixes actually get tracked.

The call we'd make #

Run a thirty-minute threat model on every feature that touches a trust boundary, and skip it on the ones that do not. Draw the DFD, walk STRIDE, and file tickets before the design doc is approved. You will catch a class of bug that no scanner will ever find, and you will catch it while it is still a diagram instead of an incident.