There is a category of security problem that no scanner will ever find. Not because the tooling is inadequate, but because the code is doing exactly what it was designed to do — and the design is the problem.
A password reset flow that emails a token but never binds it to a session is not a bug. Every line works as intended. The flaw is architectural, and the only way to catch it is to reason about the design deliberately before it becomes an implementation. That is threat modelling.
Four questions
Strip away the frameworks and the practice reduces to four questions, framed by Adam Shostack and widely adopted since:
- What are we building?
- What can go wrong?
- What are we going to do about it?
- Did we do a good job?
Everything else is technique for answering these more reliably. Teams that skip the first question produce threat models describing a system nobody actually built; teams that skip the fourth never learn whether the exercise was worth the afternoon.
Start with a diagram that shows trust boundaries
The prerequisite is a shared picture of the system, and the useful abstraction is a data flow diagram: the external entities, the processes, the data stores, the flows between them — and, critically, the trust boundaries those flows cross.
A trust boundary is any point where the privilege or trustworthiness of data changes: browser to server, application to database, your service to a third-party API, one tenant to another. They matter because virtually every interesting vulnerability lives on one. Data that stays entirely within a single trust level is rarely where the problem is.
Keep the diagram at the level where architectural decisions are visible. A whiteboard sketch that everyone in the room agrees is accurate beats a beautiful diagram that is six months stale.
STRIDE, and what each letter is really asking
STRIDE gives structure to the second question by pairing each threat category with the security property it violates. Applied per element and per flow, it turns an open-ended brainstorm into something systematic.
- Spoofing — violates authentication. Can someone claim to be a user, a service, or a system they are not? Look at every point where identity is asserted, especially service-to-service calls that assume network position implies identity.
- Tampering — violates integrity. Can data be modified in transit, at rest, or in a client the user controls? Anything computed client-side and trusted server-side belongs here, including hidden form fields, prices in a request body and JWT claims accepted without verification.
- Repudiation — violates non-repudiation. Can someone perform an action and plausibly deny it? This is where logging becomes a security requirement rather than an operational one.
- Information disclosure — violates confidentiality. Where does data go that it should not? Verbose errors, debug endpoints, over-broad API responses that return whole objects for the client to filter, and identifiers that leak in URLs and referrers.
- Denial of service — violates availability. What is expensive to compute or unbounded in size? Password hashing, report generation, regular expressions applied to user input, file uploads.
- Elevation of privilege — violates authorisation. Can a user do something reserved for another role, or reach another tenant’s data? Usually the highest-severity category, and it concentrates around object references and role checks applied inconsistently.
The value of the mnemonic is coverage. Left unstructured, teams find the threats they already expect; STRIDE forces attention onto categories that would otherwise be skipped — repudiation in particular is almost never raised spontaneously.
A short worked example
Consider a password reset. The flow: user requests reset, service emails a link containing a token, user follows it, service accepts a new password.
Applied systematically, the questions become concrete. Spoofing: can someone request a reset for another account, and does the response reveal whether that account exists? Tampering: is the token bound to the specific account, or can it be altered to apply elsewhere? Information disclosure: does the token appear in a URL, where it will land in browser history, server logs and any referrer sent to a third party on that page? Denial of service: can the endpoint be used to flood a mailbox, or to burn a target’s ability to reset at all? Elevation of privilege: does completing a reset invalidate existing sessions, or can an attacker who already holds a session retain it after the legitimate owner recovers the account?
That last one is the flaw that opened this article, and it is invisible to every scanner. It is also entirely obvious once someone asks the question out loud.
Tampering questions frequently come down to what a token actually asserts, and whether the design trusts it too readily.
Deciding what to do
Every identified threat resolves into one of four responses, and naming the choice explicitly matters as much as making it.
- Mitigate — add a control that reduces likelihood or impact. The default.
- Eliminate — remove the feature or the data. Frequently the strongest option and the least considered. Data you do not store cannot be breached.
- Transfer — shift responsibility, for instance by using a managed identity provider rather than building authentication.
- Accept — a legitimate decision when the risk is genuinely low, provided it is recorded with a rationale and an owner. Undocumented acceptance is indistinguishable from having missed it.
Prioritise by exploitability and impact rather than by category. A theoretical elevation of privilege requiring physical access matters less than an information disclosure reachable anonymously over the internet.
Making it happen in practice
The reason threat modelling is widely endorsed and narrowly practised is that heavyweight versions collapse under their own weight. A few things make it stick:
- Timebox it. Ninety minutes with the right people beats a three-week formal exercise nobody repeats.
- Include developers. Threat models produced by a security team in isolation describe a system as documented rather than as built.
- Trigger on change, not on calendar. Model when a new trust boundary appears, an authentication flow changes, or a new data classification enters the system.
- Record the model with the code. A diagram and a threat list in the repository stay closer to reality than a document in a wiki.
- Convert outcomes into tickets. Findings that do not enter the backlog do not get fixed.
- Write down what you decided not to do. The accepted risks are the most valuable output for whoever inherits the system.
Where it fits alongside testing
Threat modelling does not replace static analysis, dependency scanning or penetration testing. It addresses a different failure mode entirely. Scanners find implementation defects in code that exists. Threat modelling finds design defects before the code exists, when changing the design is still cheap.
That economic point is the whole argument. Restructuring an authorisation model during design costs a conversation. Restructuring it after launch costs a migration, a regression risk and an incident write-up. The practice earns its place not because it finds more issues than automated tooling, but because it finds the ones automation structurally cannot — at the only moment when they are still inexpensive to fix.
A catalogue of design flaws worth checking for
Experience makes threat modelling faster because the same design mistakes recur across systems. Working through a checklist of known patterns is a reasonable substitute for that experience while a team builds it.
Authentication and session design
Does authentication state carry any client-controlled component? Are tokens invalidated on password change, role change and logout? Can a session be fixated before login? Is there a path that issues a session without full authentication — a partially authenticated state during MFA, for example, that grants more than it should?
Authorisation placement
Is authorisation enforced at a single choke point or repeated in every handler? The latter guarantees an inconsistency eventually. Are checks performed on the object being accessed, or only on the endpoint being called? Does any code path retrieve an object first and check permission afterwards, leaking existence through timing or error differences?
Data handling
Where does sensitive data come to rest that nobody intended — logs, caches, error reports, analytics, backups? Is data classified so that decisions about encryption and retention have something to key on? Are identifiers used in URLs sequential and therefore enumerable?
Trust in the client
Any value computed client-side and accepted server-side is a design flaw regardless of how it is transmitted. Prices, quantities, role claims, feature flags, and validation results all belong here.
Third-party integration
What can the third party do if compromised? Are webhook callers authenticated, or does the endpoint accept any well-formed request? Does the integration hold broader permissions than its function requires?
Modelling systems that already exist
Most threat modelling in practice is applied to systems already in production, where the design decisions were made years ago by people who have left. The method adapts, with two adjustments.
First, build the diagram from reality rather than from documentation. Infrastructure inventories, network configuration, identity role assignments and actual traffic tell you what the system is; architecture documents tell you what it was intended to be, and the gap is frequently where the risk sits.
Second, prioritise ruthlessly. A legacy system will generate more findings than anyone can remediate, and producing a hundred-item list that nobody actions is worse than useless because it creates the appearance of diligence. Focus on the trust boundaries carrying the most sensitive data and the paths reachable by the least-trusted actors, and accept the rest explicitly with a written rationale.
Scaling it beyond one team
Threat modelling fails to scale when it depends on a small central security team attending every session, which becomes a bottleneck and then a rubber stamp. The alternative is to distribute the practice and keep the expertise for the hard cases.
What tends to work: a lightweight template teams complete themselves; a short list of triggering events that require a model rather than a blanket policy; a security champion embedded in each team who has done it several times; and central review reserved for systems handling the most sensitive data or introducing genuinely novel architecture.
Measure adoption by whether findings become tickets and tickets become fixes, not by how many models exist. A modest model that changed one design decision is worth more than a comprehensive one filed and forgotten.