Threat Modeling with STRIDE: From a Whiteboard Diagram to Defenses in Code
By Sergey Nosov
7 October 2026
Suppose a records API checks every caller’s token and still lets Mallory delete Alice’s records. Every test passes, because the code does exactly what someone wrote. Nobody wrote down who may delete what. That is a design flaw, and the cheapest place to find it is a whiteboard. There it costs a conversation; in production it costs an incident, a postmortem, and a customer’s trust.
Threat modeling is the practice of finding flaws like that one while they are still cheap. You do not need to be a security expert to start. You need a picture of the system and a structured way to ask what can go wrong. This article covers both: a data-flow diagram for the picture and STRIDE for the checklist, inside the frame of Adam Shostack’s four questions. It walks one small API through every step and turns the four threats that land in its code into checks in a C# handler. It ends with how to run a session in an hour.
What Threat Modeling Is
The Threat Modeling Manifesto, written by fifteen practitioners including Shostack, defines the practice as “analyzing representations of a system to highlight concerns about security and privacy characteristics.” In plainer words: before a change ships, you look at a picture of it and ask how it could be attacked and what you would do about it.
The case for doing it early is that a missing control cannot be tested into existence. The 2025 edition of the OWASP Top 10 gives insecure design a category of its own. It explains why testing cannot make up for one: “An insecure design cannot be fixed by a perfect implementation as needed security controls were never created to defend against specific attacks.” The missing ownership check in the records API is exactly that kind of control.
Threat modeling is a team activity, not a tool you run. The Manifesto values “people and collaboration over processes, methodologies, and tools,” and its answer to who should threat model is “You. Everyone.” Shostack has written a whole book about STRIDE, and he still argues that threat modeling “can and should be accessible to those without that knowledge.” My Software Development Principles series treats secure by design as a principle of its own and calls threat modeling its primary technique.
The Four Questions
The cleanest frame for the whole practice is Shostack’s Four Question Framework, and this article follows its order:
- What are we working on? Draw the system.
- What can go wrong? Walk the drawing with STRIDE, so that you do not depend on a flash of inspiration.
- What are we going to do about it? Decide, and give each decision an owner.
- Did we do a good job? Check the model and the mitigations.
In a November 2024 white paper, Shostack warns that “people commonly make the mistake of rephrasing the questions,” and the wording earns its place. “What are we working on?” scopes the model to the change in front of you. The innocent variant “What are we building?” drifts, he says, toward a waterfall view in which you must analyze the whole system before you start.
Question One: What Are We Working On?
The usual answer to the first question is a data-flow diagram. Its notation is older than threat modeling and small enough to learn in a minute. A November 2006 article in MSDN Magazine by Shawn Hernan, Scott Lambert, Tomasz Ostwald, and Shostack describes the elements:
- External entity (a rectangle): a person or system outside the scope of the model that sends or receives data, such as a user’s browser or a partner’s service. The article calls these interactors.
- Process (a circle): code you run, such as a web API.
- Data store (two parallel lines): a database, a file, a queue, or a log.
- Data flow (an arrow): data moving between the others.
- Trust boundary (a dashed or dotted line): a place where trust changes, and the most important element in the drawing. The second edition of Shostack’s Threat Modeling, due on 2 February 2027, shortens the name to “boundary.” In an August 2026 post, he explains that learners “got hung up and stayed hung up on the meaning of trust, and it didn’t serve them.”
The article defines a trust boundary as “the border between trusted and untrusted elements.” Look for every place where data or control passes between parts of the system that trust each other differently. Typical ones are the internet and your API, your API and its database, and your service and a partner’s. Data crosses those lines in both directions, and each direction has its own threats: requests can be forged, and responses can leak. Threats tend to concentrate at the boundaries, because that is where an attacker’s input meets your trust. The 2006 article makes the same call about its own example. A flow that crosses the boundary “is clearly more exposed” than one that stays inside it, and it deserves more of your effort.
Here is the system this article follows. Alice uses a browser app to manage her records. When she deletes one, the browser sends a DELETE request with her bearer token to a records API. The API deletes the row if Alice owns it and appends an entry to an audit log. The token comes from an identity provider. A complete model would draw it as another external entity; I leave it out to keep the picture small.
The numbered flows:
- A DELETE request for
/records/42, with Alice’s bearer token. - The response, such as 204 No Content or 404 Not Found.
- The delete, limited to a row the caller owns.
- The result: one row deleted, or none.
- The audit entry: who, what, and when.
- Audit entries, read during an investigation.
The three boundaries separate the internet from the API, the API from its data, and the data from the people who investigate.
Two rules of thumb from the 2006 article catch common drawing mistakes. Be careful of “magic data sources or sinks”: if data goes into a store and nothing ever reads it, the diagram is missing a reader. That rule is why the investigator is in the drawing; without a reader, the audit log would be a sink. And beware of “psychokinesis as a data transport”: data never goes straight from a person to a disk without a process in between. You do not need formal notation, either. Shostack notes that “threat modeling can be done without a flow diagram,” and a whiteboard photo with the boundaries marked is enough to start.
Question Two: What Can Go Wrong?
With the picture drawn, the second question asks what can go wrong, and STRIDE answers it with a checklist. Loren Kohnfelder and Praerit Garg introduced it in “The Threats to Our Products,” an internal Microsoft paper dated 1 April 1999. It asked every product team to use the model “to identify various types of threats the product is susceptible to during the design phase.” Twenty years after it appeared, Shostack wrote that it was revolutionary because it was “the first to structure the process of how to find threats.” He also granted that “STRIDE was not the first suggestion for a systematic approach.”
Each letter names a kind of threat, and each kind violates one security property. The 2006 article pairs them up, and the pairing is the most useful thing to memorize:
- Spoofing violates authentication: pretending to be someone, or something, you are not.
- Tampering violates integrity: changing data or code that should not change.
- Repudiation violates non-repudiation: performing an action and later denying it, with no trustworthy record to show otherwise.
- Information disclosure violates confidentiality: exposing data to someone who should not see it.
- Denial of service violates availability: making the system slow or unusable for legitimate users.
- Elevation of privilege violates authorization: gaining powers you were never granted. It ranges from reading a neighbor’s record to the worst case the 1999 paper describes, an attacker who has “become part of the trusted system itself and can do anything.”
The categories overlap, and that is fine. Faced with a threat that fits two letters, the 2006 article shrugs: “Don’t get too hung up over the terminology.” The letters exist to make you look in six directions, not to file each finding in the right drawer.
STRIDE is a security lens, and privacy harms need a privacy lens, such as LINDDUN, “a recognized privacy threat modeling framework, developed by privacy experts at KU Leuven.” One of its seven categories is non-repudiation, the very property that STRIDE asks you to add. An anonymous reporting system may need to preserve its users’ ability to deny having filed a report.
STRIDE per Element
Walking six threats against every element sounds like a lot of work. The shortcut is that not every threat applies to every kind of element. The 2006 article’s chart, which Microsoft calls STRIDE per element, narrows the list. Treat it as a starting checklist, not a rule: if you can describe a credible threat the chart leaves out, write it down anyway.
- External entities: spoofing and repudiation. What happens inside them is outside your model; someone pretending to be them, or denying what they did, is not.
- Processes: all six.
- Data stores: tampering, information disclosure, and denial of service. Add repudiation when the store is a log; the 1999 paper already warned that “tampering with security log can result in repudiability.”
- Data flows: tampering, information disclosure, and denial of service.
Look at the entry for processes. The code you write is the only kind of element exposed to all six, and that is where most of the analysis lands. The chart turns “think of everything” into a finite list per element.
Walking the Example
Here is the records API, element by element. These are the kinds of threats a first pass finds; the point is the method, not completeness.
- Browser (spoofing, repudiation): Mallory calls the API with a stolen or forged token and poses as Alice. Alice deletes a record and later says she never did.
- Flows 1 and 2 (tampering, information disclosure, denial of service): Something between the browser and the API changes the record ID in transit. Someone on the network reads the token and replays it. A flood of requests ties up the API.
- Records API (all six):
- Spoofing: another service uses the API’s database credentials and acts as the API.
- Tampering: a crafted ID changes the SQL statement.
- Repudiation: the API deletes a record without recording who asked.
- Information disclosure: an error response shows a stack trace, or a 403 for other people’s records and a 404 for missing ones tell other users which IDs exist.
- Denial of service: a burst of slow requests holds every connection in the pool.
- Elevation of privilege: Mallory, signed in as herself, deletes Alice’s record by changing the ID in the URL.
- Records database (tampering, information disclosure, denial of service): Someone with direct database access edits rows and bypasses the API. A backup or a test copy leaks. A full disk stops every delete.
- Audit log (tampering, repudiation, information disclosure, denial of service): An intruder erases their entries. A log that anyone can edit proves nothing. Someone who should not read the log reads it, and it holds more personal data than it needs. When the log fills up, deletes either fail or go unrecorded.
- Investigator (spoofing, repudiation): Someone poses as an investigator to read the log. An investigator denies having searched it.
- Flows 3 to 6 (tampering, information disclosure, denial of service): someone reads or alters the traffic to and from the data stores, or the network path between them fails.
Notice where most of them gather: at the boundary between the browser and the API, and inside the API itself.
Question Three: What Are We Going to Do about It?
Each kind of threat violates a property, so each has a matching family of controls that restore it. The 2006 article makes the same point: recasting its threat-to-property table “in terms of available technologies gives you an idea of what kinds of mitigations are necessary.”
- Spoofing → authentication: multifactor sign-in, signed tokens, mutual TLS, and managed identities for services.
- Tampering → integrity: input validation, parameterized queries, signatures, and access control on files and tables.
- Repudiation → non-repudiation: audit logs that the audited code cannot change, signed records, and reliable timestamps.
- Information disclosure → confidentiality: encryption in transit and at rest, least privilege, and error messages that say little.
- Denial of service → availability: rate limits, quotas, timeouts, autoscaling, and redundancy.
- Elevation of privilege → authorization: least privilege, server-side authorization checks on every request, and sandboxing.
Not every threat deserves a mitigation. Shostack lists the options plainly: “You can mitigate, eliminate, accept, or transfer risk.” Eliminating means removing the feature that creates the threat. His example is a signup flow that collects pictures of IDs, which “adds a huge burden to secure the data that we never use again.” Transferring hands the risk to another party; the OWASP cheat sheet on threat modeling describes it as allocating “contractual or financial responsibility to another party.” Accepting is legitimate when it is a decision: write it down with an owner and a date to revisit it. The cardinal sin is accepting a risk by accident, by never asking the question.
A long list needs an order before it needs decisions. The output of a session is a prioritized list, and a rough high, medium, or low for impact and for likelihood is enough to put the worst threats first.
For the records API, every threat from the walk-through gets a decision:
- Mitigate in the handler: the forged token, Mallory’s ID swap, the injection, and the missing audit entry. The next section shows all four.
- Mitigate elsewhere:
- stolen tokens: short lifetimes now, with sender-constrained tokens on the backlog;
- changes and eavesdropping in transit, including an ID altered on the way: TLS on every connection;
- the shared database password: a per-service identity that may insert audit rows but never change them;
- stack traces: error responses that say little;
- pool exhaustion: command timeouts shorter than the 30-second default, and a cap on concurrent requests;
- readers of the log: named investigators with their own accounts, whose searches are logged too;
- personal data in the log: IDs rather than names, kept only as long as investigations need them;
- leaked copies: encrypted backups, and no production data in test environments;
- full disks: storage alerts that someone owns;
- floods and network failures: rate limits at the gateway, and redundant paths to the database.
- Accept for now: a database administrator who edits records or audit rows directly, until both tables become tamper-evident.
In a real model, every line also gets an owner. Written down, the acceptance fits on a card:
Element: Records database
Threat: Tampering. A database
administrator edits
records or audit rows
directly.
Decision: Accept until both
tables become ledger
tables.
Owner: Records API lead
Review: 1 March 2027
Four Threats in One Handler
Here are the four threats that land in the API’s own code, defended in one ASP.NET Core endpoint. It uses the Microsoft.AspNetCore.Authentication.JwtBearer and Microsoft.Data.SqlClient packages. The setup comes first, because authentication runs before any handler. With no arguments, AddJwtBearer() reads its settings, such as the authority and the valid audiences, from the Authentication:Schemes:Bearer section of the app’s configuration:
var connectionString = builder.Configuration.GetConnectionString("Records");
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddScoped(_ => new SqlConnection(connectionString));
builder.Services.AddSingleton(TimeProvider.System);
The handler then takes the threats in order:
app.MapDelete("/records/{id:int}", async (int id, ClaimsPrincipal user,
SqlConnection db, TimeProvider clock) =>
{
// S, spoofing: RequireAuthorization() below admits only callers whose
// bearer token passed validation (signature, issuer, audience, and
// expiry) and names a user. Everyone else gets 401 or 403.
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier)!;
await db.OpenAsync();
await using var tx = (SqlTransaction)await db.BeginTransactionAsync();
// E, elevation of privilege: authentication is not authorization.
// The WHERE clause deletes the record only if this caller owns it,
// so the check and the delete are one step, and OUTPUT returns the
// ID only if a row was really deleted. A missing record and another
// user's record both get 404, so callers cannot probe for IDs.
await using var delete = new SqlCommand(
"DELETE FROM Records OUTPUT DELETED.Id " +
"WHERE Id = @id AND OwnerId = @owner", db, tx);
// T, tampering: both values are bound as typed parameters, so input
// can never become part of the SQL text.
delete.Parameters.Add("@id", SqlDbType.Int).Value = id;
delete.Parameters.Add("@owner", SqlDbType.NVarChar, 255).Value = userId;
if (await delete.ExecuteScalarAsync() is null) return Results.NotFound();
// R, repudiation: record who did what, and when, in the same
// transaction, so that no delete commits without its audit row.
await using var audit = new SqlCommand(
"INSERT INTO AuditLog (UserId, Action, RecordId, At) " +
"VALUES (@user, 'record.delete', @id, @at)", db, tx);
audit.Parameters.Add("@user", SqlDbType.NVarChar, 255).Value = userId;
audit.Parameters.Add("@id", SqlDbType.Int).Value = id;
audit.Parameters.Add("@at", SqlDbType.DateTimeOffset).Value =
clock.GetUtcNow();
await audit.ExecuteNonQueryAsync();
await tx.CommitAsync();
return Results.NoContent();
}).RequireAuthorization(policy => policy
.RequireAuthenticatedUser()
.RequireClaim(ClaimTypes.NameIdentifier));
- Spoofing. The JWT bearer handler checks each token before the endpoint runs, and
RequireAuthorization()turns away anyone without a valid one. Its policy also demands an authenticated user with a user ID, so a valid token that names nobody gets 403. Microsoft’s guidance lists what “should be validated”: the signature, the issuer, the audience, and the expiry. Two limits remain. By default, a token stays acceptable for five minutes past its expiry, to allow for clock skew. And validation cannot tell a stolen token from a legitimate one, because a bearer token works for whoever holds it. Short lifetimes limit the damage, and sender-constrained tokens, which the same guidance describes, “require the requesting client to prove possession of a private key to use the token.” - Elevation of privilege. Authentication is not authorization: Mallory has a perfectly valid token. The OWASP API Security Top 10 puts broken object level authorization first in its 2023 edition and rates its prevalence as widespread. Its rule is simple: “Every API endpoint that receives an ID of an object, and performs any action on the object, should implement object-level authorization checks.” The handler puts that check inside the DELETE statement, so the check and the delete are one step and nothing can change between them. A record that belongs to someone else matches no row, just like a record that does not exist, and both get 404. GitHub answers the same way “to avoid confirming the existence of private repositories,” and Microsoft’s guidance notes that HTTP “also allows returning 404 for not authorized.” In a larger app, give the rule one home, so that every endpoint asks the question the same way.
- Tampering. The ID and the owner reach SQL Server as typed parameters, never as part of the statement text, so nothing a caller sends can change the statement. The route constraint
{id:int}adds a second layer: a path such as/records/1 OR 1=1never reaches the handler. - Repudiation. The audit row commits in the same transaction as the delete, so no delete commits without a record of who asked. The row records the account whose token the API accepted, not the person behind it. For actions that must survive a determined denial, the 1999 paper’s remedy still applies: “message signatures and signature verification,” with the signature bound to the specific action. An insert alone is not tamper-evident, either. At a minimum, let the service’s database user insert audit rows but not update or delete them. On SQL Server 2022 and later and on Azure SQL, ledger tables go further. An append-only ledger table allows “only INSERT operations,” even for database administrators. An updatable ledger table keeps the history of every change to a table such as Records, including changes made around the API. Microsoft says that ledger “guarantees that any tampering will be detected when the ledger data is verified,” provided the digests used for verification are stored out of an attacker’s reach.
- The other two letters are handled mostly outside this handler. TLS and encryption at rest protect data on the wire and on disk, and rate limits at the gateway cap request volume. Neither stops a signed-in user from reading too much or a slow query from blocking others, which is why authorization, data minimization, and timeouts appear among the decisions. In the handler itself, the 404 closes one disclosure channel.
Two details in the delete matter more than they look. First, the handler learns whether a row went from OUTPUT DELETED.Id, not from a row count. When a connection runs with SET NOCOUNT ON, the count from ExecuteNonQuery comes back as -1. A check for zero then lets Mallory through, with a 204 and an audit row for a deletion that never happened. Second, the ownership comparison has to be exact. OIDC defines the subject as “a case-sensitive string” of at most 255 ASCII characters that is unique only within its issuer. Give the owner column a binary collation and room for 255 characters, and store the issuer too if you trust more than one. Under my test database’s default case-insensitive collation, a token for ALICE deleted Alice’s record. These are the tables I tested against:
CREATE TABLE Records (
Id int PRIMARY KEY,
OwnerId nvarchar(255) COLLATE Latin1_General_100_BIN2 NOT NULL,
Title nvarchar(200) NOT NULL);
CREATE TABLE AuditLog (
Id bigint IDENTITY PRIMARY KEY,
UserId nvarchar(255) COLLATE Latin1_General_100_BIN2 NOT NULL,
Action nvarchar(64) NOT NULL,
RecordId int NOT NULL,
At datetimeoffset NOT NULL);
I ran the endpoint as a .NET 10 file-based app against SQL Server LocalDB, with JwtBearer 10.0.12, Microsoft.Data.SqlClient 7.1.1, and test tokens signed by a local key:
- 401: no token, or a token that was signed by an untrusted key, named another audience, came from another issuer, or had expired thirty minutes earlier. A token that had expired two minutes earlier still worked, as the clock-skew default allows.
- 403: a valid token that named no user.
- 404, with nothing deleted: Mallory’s attempt on Alice’s record, a nonexistent ID, injection text in the URL or in the token’s subject, and a token for
ALICE. - 204 and exactly one audit row: Alice’s own delete, and one of five simultaneous deletes of a single record. The other four got 404.
- 500, with nothing deleted and no audit row: a delete whose audit insert I forced to fail.
The results were the same with SET NOCOUNT ON forced on every connection. A two-step version looks up the owner and then deletes by ID without checking how many rows went. It failed the concurrency test once I stretched the gap between its two steps to 200 milliseconds: all five requests got 204 and wrote five audit rows for one deletion. Checking the row count would stop the duplicates, but an ID-only delete still leaves a gap in which ownership can change. The single statement closes both.
Question Four: Did We Do a Good Job?
The 2006 article offers a test for completeness. Keep going until you have addressed:
- tampering, information disclosure, and denial of service against every data flow and data store;
- all six threats against every process;
- spoofing and repudiation against every external entity;
- any threats unique to your boundaries.
Its authors add a fair warning: “None of this will guarantee that the system is secure, but it will certainly help you sleep better at night.”
Completeness is half the question. The other half is whether the mitigations are real. A mitigation that exists only in the threat model is a wish. Turn each one into a backlog item and a test, and check in code review that it landed. OWASP’s advice for authorization holds for every letter: “Write tests to evaluate the vulnerability of the authorization mechanism. Do not deploy changes that make the tests fail.” The tests in the previous section are that kind.
Last, ask the people. For teams adopting the practice, Shostack suggests two simple questions: “Would you threat model again? Would you recommend threat modeling to your colleagues?” A no to either is a reason to ask why.
Running a Session in an Hour
Threat modeling should be light enough to do often. Here is how I would run a session:
- Bring the right people: a developer who will build the change, someone security-minded, and whoever knows how the system really behaves.
- Draw for five minutes. Sketch the part you are changing on a whiteboard, with its boundaries marked.
- Walk the boundaries first, then take each element through the STRIDE-per-element chart.
- Decide, but do not design. Give each threat a decision and an owner, and leave the engineering of fixes to the backlog. Shostack observes that counting the fixes as part of threat modeling “contributes to the perception that threat modeling is heavyweight and time-consuming.”
- Stop at the hour. A timebox keeps the session cheap enough to repeat.
- Keep the model current. Revisit it when the design changes, rather than filing it as a one-time deliverable.
Teams that work in short iterations can go further. Izar Tarandach’s Continuous Threat Modeling, which Shostack sums up as “threat model every story,” brings the practice into each iteration. The OWASP Top 10 makes a similar recommendation: “Threat modeling should be integrated into refinement sessions (or similar activities).”
Tools That Help
- Microsoft Threat Modeling Tool is “a free click-to-download application for Windows.” You draw the diagram from a template, and the tool generates threats for each element, with what Microsoft calls “guided analysis of threats and mitigations.” As of October 2026, the latest release is 7.3.51110.1, from 10 November 2025.
- OWASP Threat Dragon is “a free, open-source, cross-platform threat modeling application” for drawing diagrams and listing threats for their elements. It is primarily a web application, with desktop versions for Windows, macOS, and Linux. Version 2.6.2 appeared on 10 May 2026.
- pytm, also from OWASP, lets you describe the system in Python. From that model it generates a data-flow diagram, a sequence diagram, and a list of relevant threats. The threat model can then live in the repository and change through pull requests. An optional check warns when the code a model describes is newer than the model by more than a set number of days. Version 1.4.0 appeared on 6 July 2026.
The tool is optional; the habit is not.
When to Reach for It
You do not need to threat model everything. Reach for it at four moments:
- A new feature or service, especially one that adds a boundary: a new endpoint, integration, or data store.
- An architecture change, such as a new authentication flow, a third-party connection, or data moving across a boundary. Last quarter’s model no longer matches reality.
- Before a security review, audit, or penetration test. Walking in with a model turns the review into a conversation about mitigations instead of a scramble.
- Sensitive data, such as personal data, credentials, or anything regulated, which raises the stakes on disclosure and tampering.
OWASP recommends threat modeling “for critical parts of the application such as authentication, access control, business logic, and key flows.” NIST’s Secure Software Development Framework (SP 800-218, version 1.1) makes risk modeling, threat modeling included, a task of its own: PW.1.1.
Checklist
- Ask the four questions in Shostack’s words: What are we working on? What can go wrong? What are we going to do about it? Did we do a good job?
- Sketch the system first, and mark every boundary. You cannot reason about threats you cannot see, and five minutes at a whiteboard is enough to start. Spend your attention at the boundaries first.
- Use STRIDE per element as the checklist. External entities get spoofing and repudiation, and processes get all six. Flows and stores get tampering, information disclosure, and denial of service, and logs get repudiation too.
- Make every threat a decision: mitigate, eliminate, transfer, or accept, with an owner, and with a date for every acceptance.
- Remember that authentication is not authorization. Check the caller’s permission for the operation on that specific object, and make sure the answer cannot change between the check and the action.
- Test the mitigations, and keep the tests in the build.
- Make it a habit, not an event: a rough model on every new feature, revisited when the design moves.
- Start small: pick one service you own, mark its boundaries, and name the STRIDE threat you are least sure is covered.
Further Reading
- Understanding the Four-Question Framework for Threat Modeling (Adam Shostack, Shostack + Associates, November 2024)
- Threat Modeling Manifesto (Threat Modeling Manifesto working group)
- Uncover Security Design Flaws Using the STRIDE Approach (Shawn Hernan, Scott Lambert, Tomasz Ostwald, and Adam Shostack, MSDN Magazine, November 2006)
- 20 Years of STRIDE: Looking Back, Looking Forward (Adam Shostack, 1 April 2019)
- Threat Modeling Cheat Sheet (OWASP Cheat Sheet Series)