Start with four questions: what are we working on, what can go wrong, what will we do about it, and did we do a good enough job. Draw a data-flow diagram with trust boundaries. Apply STRIDE per element to technical designs, attack trees to one high-value attacker goal, LINDDUN to personal data, and PASTA when risk must be ranked by business impact. Every threat you act on needs an owner, a control and a test.
- Do scope each session to one feature or change; a model of everything never finishes.
- Do draw trust boundaries first; the most useful threats sit on flows that cross them.
- Do use STRIDE letters as prompts, not as a classification exercise.
- Do add LINDDUN when the feature collects or shares personal data; STRIDE does not ask privacy questions.
- Don't treat a tool-generated threat list as the model; business-rule threats come from people.
- Don't close a threat without a response, an owner and a test or check that proves the control.
- Compare the data-flow diagram with the deployed system: every store has a writer and a reader, and every call to another network or account crosses a drawn boundary.
- For each threat marked mitigated, find the merged change and a test that fails when the control is removed.
- Run the abuse-case tests in CI on every pull request that touches the modeled feature.
- Confirm each accepted or transferred risk names an owner with authority to accept it and a date to revisit it.
- Re-run the session when a story adds a trust boundary, data store, external integration, role or new class of personal data.
- Applies to
- OWASP pytm 1.4.0 (Python 3.11 or later)
- Node.js Tested on 26.3
- Express 5.x (tested on 5.3.0)
Design reviews for new features and systems. A threat model helps only when its threats become tests, tickets or design changes.
- Part of
- Workflow: Review a design
Learning goals and prerequisites
What you will learn
- Run a session with the four-question frame and a data-flow diagram.
- Apply STRIDE per element and per interaction, and know when to switch to attack trees, LINDDUN or PASTA.
- Turn threats into records with responses, owners and abuse-case tests.
Before you start
Developers, architects, testers and AppSec engineers who review designs or run threat modeling sessions.
- Read a simple architecture or data-flow diagram.
- Know the basics of web API authentication and authorization.
Threat modeling means examining a model of a system to find security and privacy problems while they are still cheap to fix. The Threat Modeling Manifesto organizes the work around four questions: what are we working on, what can go wrong, what are we going to do about it, and did we do a good enough job. STRIDE, attack trees, LINDDUN and PASTA are different ways of answering the second question, with more or less structure around the other three.
Each question should leave something behind. NIST SSDF 1.1 (SP 800-218) asks teams to use risk modeling such as threat modeling (PW.1.1) and to track the resulting requirements, risks and design decisions (PW.1.2). OWASP Top 10:2025 A06 Insecure Design recommends threat modeling for authentication, access control, business logic and other key flows, done during refinement rather than after release.
| Question | Activity | Output |
|---|---|---|
| What are we working on? | Scope the change, draw a data-flow diagram, list assumptions | Diagram, scope, assumptions |
| What can go wrong? | Apply STRIDE, an attack tree, LINDDUN or PASTA | Threat list |
| What are we going to do about it? | Mitigate, eliminate, transfer or accept each threat; assign owners | Requirements, tickets, abuse-case tests, accepted risks |
| Did we do a good enough job? | Check the model against the real system and run the tests | Review record, passing tests, revisit triggers |
No method finds everything, and the SEI comparison of twelve methods recommends choosing by the project's needs and combining methods when one view is not enough. A technical design review starts with STRIDE; a fraud path calls for an attack tree; personal data adds LINDDUN; a risk decision that leadership must fund may justify PASTA.
| Method | Use it when | Unit of analysis | Effort | Main output |
|---|---|---|---|---|
| STRIDE per element | Reviewing a technical design with clear components | Each element in the diagram | Low to medium | Threats per element |
| STRIDE per interaction | Risk sits between components: webhooks, approvals, protocols | Each origin, destination and data exchanged | Medium | Threats per interaction |
| Attack tree | One high-value goal such as fraud, account takeover or key theft | The goal, split into AND and OR steps | Medium | Attack paths and the controls that cut them |
| LINDDUN GO or PRO | The feature collects, links or shares personal data | A card deck (GO) or each interaction (PRO) | GO low, PRO higher | Privacy threats and mitigations |
| PASTA | Risk must be ranked by business impact, with threat intelligence | The whole application, in seven stages | High | Risk-ranked threats with impact |
| Per-story check | A story touches a trigger such as auth, money or a new boundary | Only the change | 15 to 45 minutes | Acceptance criteria and abuse-case tests |
A data-flow diagram has four kinds of element plus boundaries. External entities are people and systems you do not control. Processes are code you run. Data stores hold data at rest, including queues, buckets and logs. Data flows are the arrows between them. A trust boundary marks where the level of trust or privilege changes: internet to API, service to database, your cloud account to a SaaS provider, or a sandbox around a file parser.
Microsoft's SDL guidance gives useful sanity checks. Data does not appear from nowhere, so every store needs a process that writes it and one that reads it. Data never moves straight from a person to a disk; a process sits between them. Collapse similar elements inside one boundary, and use a context diagram plus detail diagrams instead of one huge picture. Add the paths teams forget: background jobs, admin and support tools, and third-party callbacks.
Keep the model next to the code. OWASP pytm describes the diagram in Python and generates the diagram and candidate threats from it. OWASP Threat Dragon and the Microsoft Threat Modeling Tool are diagram-first alternatives. The model below describes the worked example used in the rest of this guide.
#!/usr/bin/env python3
# Expense-claims API threat model (pytm 1.4.0, Python 3.11+)
from pytm import (TM, Actor, Boundary, Classification, Data, Dataflow,
Datastore, ExternalEntity, Process)
tm = TM("Expense claims API")
tm.description = "Employees submit claims with receipts; managers approve; payroll pays."
tm.isOrdered = True
internet = Boundary("Internet")
cloud = Boundary("Company cloud account")
sandbox = Boundary("Receipt parsing sandbox")
payroll_net = Boundary("Payroll provider")
employee = Actor("Employee", inBoundary=internet)
manager = Actor("Manager", inBoundary=internet)
payroll = ExternalEntity("Payroll provider API", inBoundary=payroll_net)
api = Process("Expenses API", inBoundary=cloud)
worker = Process("Receipt worker", inBoundary=sandbox)
claims_db = Datastore("Claims DB (claims, approvals, audit)", inBoundary=cloud,
storesPII=True, maxClassification=Classification.RESTRICTED)
receipts = Datastore("Receipt object store", inBoundary=cloud, storesPII=True)
claim = Data("Claim and receipt image", classification=Classification.RESTRICTED)
Dataflow(employee, api, "Submit claim + receipt", protocol="HTTPS", data=claim)
Dataflow(api, receipts, "Store receipt")
Dataflow(api, claims_db, "Write claim version")
Dataflow(manager, api, "Approve claim version", protocol="HTTPS")
Dataflow(receipts, worker, "Read receipt for extraction")
Dataflow(worker, claims_db, "Write extracted fields")
Dataflow(api, payroll, "Payout instruction", protocol="HTTPS")
Dataflow(payroll, api, "Payout status webhook", protocol="HTTPS")
if __name__ == "__main__":
tm.process()STRIDE names six threat types, each the violation of one property: spoofing breaks authentication, tampering breaks integrity, repudiation breaks non-repudiation, information disclosure breaks confidentiality, denial of service breaks availability, and elevation of privilege breaks authorization. Microsoft's per-element chart limits which questions to ask of each element type, which keeps a session moving.
Treat the letters as prompts and argue about the threat, not its category. A small diagram yields dozens of candidates, many already covered by platform controls such as TLS everywhere; record those once as assumptions. If a data store holds audit logs, ask repudiation questions too, because whoever can edit the log can erase the evidence. For the patterns that recur per element type, such as caller-supplied identity on a boundary flow or a missing tenant filter on a shared store, use /domains/appsec/threat-modeling/threat-pattern-library/.
| Element type | Ask about | Example question |
|---|---|---|
| External entity | S, R | Can someone pose as this user or partner, or deny an action later? |
| Process | S, T, R, I, D, E | Can input change its behavior, read its data, exhaust it or borrow its privileges? |
| Data store | T, I, D | Who can change, read, fill or delete it? |
| Data flow | T, I, D | Can traffic be altered, read or blocked in transit? |
STRIDE per interaction walks each interaction, meaning an origin, a destination and what passes between them, instead of each element. It suits threats that exist only in the combination. A payout webhook is only as trustworthy as the sender check on that flow, which /domains/appsec/secure-features/verify-webhook-deliveries/ covers. An approval means something only if it binds to exactly what the approver saw, which neither the manager nor the API owns on its own.
A replicated controlled experiment with student teams found no significant difference in precision or productivity between the two variants (Mbaka and Tuma, 2022). Choose by fit: per element for coverage of a new component, per interaction for integrations, protocols and approval chains.
An attack tree puts the attacker's goal at the root and the ways to reach it underneath. OR nodes are alternatives; AND nodes are steps that must all succeed. Schneier's 1999 description assigns values to the leaves and propagates them upward: an OR node takes its easiest or cheapest child, and an AND node needs every child, so costs add up. Use a tree for fraud, account takeover or signing-key theft, where attackers chain steps that a per-element pass lists separately.
Read the tree again after the fixes. In this example only the collusion branch survives the technical controls. That is a business decision, such as a second approver above a limit plus finance sampling, recorded as an accepted risk with an owner.
GOAL: Get paid for money that no approver agreed to pay [OR]
+-- 1. Approve your own claim [OR]
| +-- 1.1 Call /approve on your own claim T2
| +-- 1.2 Create the claim with status=approved T3
+-- 2. Change the amount after approval [AND]
| +-- 2.1 Approval does not record the version reviewed T4
| +-- 2.2 Edits are still accepted after approval T4
+-- 3. Trigger a second payout [AND]
| +-- 3.1 Post a forged "payout failed" webhook T1
| +-- 3.2 Retry logic resends without asking the provider T1
+-- 4. Collude with the approving manager (insider) T8
Leaf value: possible without insider help? (yes/no)
OR node = yes if any child is yes. AND node = yes only if all children are yes.
After T1-T4 are fixed, only branch 4 is still possible, and it needs an insider.STRIDE asks whether an attacker can break a security property. It does not ask whether the system collects, links or exposes personal data while working exactly as designed. LINDDUN, from KU Leuven's DistriNet group, covers seven privacy threat types: linking, identifying, non-repudiation, detecting, data disclosure, unawareness and unintervenability, and non-compliance. Its non-repudiation is a privacy harm, being unable to deny something attributed to you, which is the opposite viewpoint from STRIDE's.
LINDDUN GO is a deck of 33 threat cards, redesigned in 2024, for a lean team session. LINDDUN PRO walks each interaction in the diagram, considering sending, transfer and receipt, with threat trees and a mapping table. Choose PRO when you need systematic, documented coverage.
Applied to receipts: images show card digits, places and sometimes health-related purchases, so keeping them forever is data disclosure and likely non-compliance. An export of merchant, time and place per employee allows linking and tracking. Employees who cannot see or correct what is kept face unawareness and unintervenability. Responses: extract only needed fields, mask card numbers, set a retention period (/domains/identity-crypto/privacy/data-minimization-retention/ covers how), limit access to approvers and finance reviewers, aggregate exports and publish a notice.
PASTA, the Process for Attack Simulation and Threat Analysis, was developed by Tony UcedaVélez in 2012. It is risk-centric and runs in seven stages: define objectives, define technical scope, decompose the application, analyze threats, analyze vulnerabilities and weaknesses, model attacks, and analyze risk and impact. It draws on business impact analysis, threat intelligence and existing vulnerability data, and it uses data-flow diagrams in stage three and attack trees in stage six.
The cost is time and people: business owners, operations and architects take part, and the output is a ranked risk register, not a quick list. Use it for a payments platform or a regulated product where leadership must weigh spending against expected loss, and keep STRIDE for feature work. In the expense system, stage one would put a figure on fraud loss and stage seven would rank the surviving fraud paths against it.
Agile teams get most of the value from short, frequent sessions scoped to one change. Hold one in refinement when a story matches any trigger below. Invite the developer, a tester, the product owner and a security champion; sketch the change on the story's diagram, walk STRIDE over the new elements and flows, and turn every threat you keep into an acceptance criterion and a named abuse-case test. Update the system diagram when the story merges. New systems and large redesigns still need a full session.
- Adds or changes a trust boundary or an external integration.
- Changes authentication, authorization, roles or tenant rules.
- Adds a data store or a new class of sensitive or personal data.
- Parses untrusted files, documents or formats.
- Moves money or performs an action that cannot be undone.
- Adds an admin, support, batch or migration path.
- Creates, stores or uses keys, tokens or other secrets.
Employees submit claims with receipt images. Their manager approves a claim, and the API sends a payout instruction to a payroll provider, which reports the result through a webhook. A worker in a sandbox extracts fields from receipts. The boundaries are the internet, the company cloud account, the parsing sandbox and the payroll provider.
Running the model above with pytm 1.4.0 produced 118 candidate findings of 43 generic types, such as interception and content spoofing. None of them concerned who may approve which claim version. The threats that matter most came from the session.
| ID | Found by | Threat | Response | Verification |
|---|---|---|---|---|
| T1 | STRIDE S, payroll provider | Forged "payout failed" webhook triggers a second payment | HMAC over timestamp and body, 5-minute window; confirm status with the provider before acting | Unsigned, stale and altered webhooks get 401 |
| T2 | STRIDE E, API | Employee approves their own claim | Only the claimant's manager may approve, never the claimant | Self-approval gets 403 |
| T3 | STRIDE T, submit flow | Client sends status=approved on create | Allowlist input fields; the server owns status and version | Create with status gets 400 |
| T4 | Per interaction, approve | Amount edited after approval, and the edited amount is paid | Approval names a version; edits after approval are refused | Stale approval and late edit get 409 |
| T5 | STRIDE I, API | Any employee reads any claim by ID | Owner or owner's manager only | Other employee gets 404 |
| T6 | STRIDE E and D, worker | Malicious PDF exploits the parser or exhausts memory | Size and page limits; sandbox with no network access | Oversized upload rejected; egress blocked |
| T7 | STRIDE R, approve flow | Manager denies approving a claim | Append-only audit event with actor, claim and version | Approval writes an audit event |
| T8 | Attack tree, branch 4 | Employee and manager collude | Accept: second approver above a limit, finance sampling | Quarterly sample review |
Name tests after threat IDs so a failing build points back to the record. The tests below run with Node.js's built-in test runner against the service's HTTP interface. All five failed against a variant of the service with the controls removed and passed against the fixed one. A check that only runs on a laptop decays; run these on every pull request that touches claims, approval or payout code. Ready-made abuse cases with tests for common features are in /domains/appsec/threat-modeling/abuse-case-library/, and /domains/appsec/threat-modeling/security-requirements-definition-of-done/ shows how to keep them as requirements with a Definition of Done.
// abuse-cases.test.js: one test per threat record (run: node --test)
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { createApp } from './app.js';
let server, base;
before(async () => {
server = createApp({ webhookSecret: 'test-only-secret' }).listen(0);
await new Promise((r) => server.once('listening', r));
base = `http://127.0.0.1:${server.address().port}`;
});
after(() => server.close());
const call = (method, path, token, body) => fetch(base + path, {
method,
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
body: body && JSON.stringify(body),
});
const submit = async (token, amount) => (await call('POST', '/claims', token, { amount })).json();
test('T2: an employee cannot approve their own claim', async () => {
const c = await submit('tok-alice', 120);
assert.equal((await call('POST', `/claims/${c.id}/approve`, 'tok-alice', { version: 1 })).status, 403);
});
test('T3: the client cannot set status when creating a claim', async () => {
const res = await call('POST', '/claims', 'tok-alice', { amount: 120, status: 'approved' });
assert.equal(res.status, 400);
});
test('T4: approval binds to the reviewed version', async () => {
const c = await submit('tok-alice', 120);
await call('PATCH', `/claims/${c.id}`, 'tok-alice', { amount: 1200 }); // now version 2
assert.equal((await call('POST', `/claims/${c.id}/approve`, 'tok-mo', { version: 1 })).status, 409);
assert.equal((await call('POST', `/claims/${c.id}/approve`, 'tok-mo', { version: 2 })).status, 200);
assert.equal((await call('PATCH', `/claims/${c.id}`, 'tok-alice', { amount: 5 })).status, 409);
});
test('T5: another employee cannot read the claim', async () => {
const c = await submit('tok-alice', 120);
assert.equal((await call('GET', `/claims/${c.id}`, 'tok-bob')).status, 404);
assert.equal((await call('GET', `/claims/${c.id}`, 'tok-mo')).status, 200);
});
test('T1: an unsigned payroll webhook is rejected', async () => {
const res = await fetch(`${base}/webhooks/payroll`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-payroll-timestamp': String(Math.floor(Date.now() / 1000)) },
body: JSON.stringify({ claim: '1', status: 'failed' }),
});
assert.equal(res.status, 401);
});Keep one short record per threat you act on: scenario, impact, response, control, owner, status, verification and the change that should reopen it. OWASP's cheat sheet lists four responses: mitigate, eliminate, transfer and accept. Transfer moves responsibility, not the harm itself, so name who still owns the residual controls.
To answer whether you did a good enough job, check that the diagram matches the deployed system, every boundary-crossing flow was considered, every threat has a response, mitigations have passing tests, and accepted risks were approved by someone with the authority to accept them.
# threats/T4.yaml: one record per threat the team decided to act on
id: T4
title: Claim amount changes after approval
found_by: STRIDE per interaction (Manager -> Expenses API, approve)
category: Tampering
scenario: >
Alice submits a claim for 120. Mo approves it. Alice edits the amount
to 1,200 before payout, and payroll pays the edited amount.
impact: A payout that no approver saw.
response: mitigate # mitigate | eliminate | transfer | accept
control: >
Approval names the version the manager reviewed. Edits after approval
are refused. Payout reads approvedVersion only.
owner: payments-team
status: verified
verification: "abuse-cases.test.js, test 'T4: approval binds to the reviewed version'"
revisit_when: Claim editing, approval or payout code changes.Most failed sessions fail in the same few ways.
- Modeling the whole estate at once. Scope to a change and finish it.
- Treating generated findings as the model. Tools miss business rules such as approval binding.
- Debating whether a threat is tampering or elevation. The category does not change the fix.
- Leaving out asynchronous jobs, admin tools and partner callbacks, where many real defects sit.
- Relying on one expert. The Manifesto calls this the hero threat modeler; everyone on the team can contribute.
- Filing the diagram and never revisiting it. Tie reviews to the triggers above, not to a calendar.
Guided practice
Threat model a proposed change to the expense-claims API: managers can delegate approval while on leave.
Supplied scenario material
Baseline: the expense-claims API from the worked example, with T1-T5 mitigated.
Change request CR-42, manager delegation:
- A manager may delegate approval rights to one employee for up to 30 days.
- New endpoint POST /delegations {delegate, from, to}; rows stored in the claims DB.
- POST /claims/:id/approve accepts the claimant's manager OR an active delegate of that manager.
- A nightly job emails each delegate the pending claims, with receipt thumbnails attached.
People: Mo manages Alice and Bob. Mo delegates to Alice for 2026-11-01..2026-11-14.
Events: Alice submits claim C9 (amount 300) on 2026-11-03. Bob submits C10 (amount 280) on 2026-11-04.
Policy: nobody may approve their own claim; every approval must be attributable to a person.1. Draw the delta diagram
List the new elements and flows CR-42 adds and the trust boundaries they cross.
Expected result and reasoning
New: a delegations data store, a create-delegation flow from the manager, an approval check that reads delegations, a nightly job process and a flow to an external email provider. The email flow crosses from the company account to a third party and carries personal data in the thumbnails.
2. Apply STRIDE per element to the new parts
For the delegation endpoint, the delegations store and the nightly job, write at least one threat each.
Expected result and reasoning
Examples: an employee creates a delegation naming themselves for Mo if the endpoint does not check that the caller is the manager (spoofing or elevation); delegation dates are edited to extend the window (tampering); thumbnails go to the email provider and inboxes (information disclosure); an approval does not show it was made by a delegate (repudiation).
3. Apply STRIDE per interaction to the approval
Analyze the interaction 'Alice, as Mo's delegate, approves C9'.
Expected result and reasoning
The rule must exclude the claimant whatever the delegation says, so Alice approving C9 is refused. The delegation window is checked at approval time. Alice approving Bob's C10 is allowed by the stated rules, and the audit event records Alice as approver acting for Mo.
4. Build an attack tree for self-approval
Root goal: 'C9 is paid without independent approval'. Add OR and AND branches and mark which survive the controls from step 3.
Expected result and reasoning
Branches include direct self-approval through delegation (blocked by the claimant rule); creating a delegation from Mo's stolen session (needs session theft, covered by existing login controls); and peer collusion, where Alice as delegate approves Bob's inflated C10 in return for a favor. Delegation makes that last branch possible, so it needs a business control such as finance sampling of delegated approvals or a rule against delegating to a direct report.
5. Write records and tests
Write two threat records in the T4 format and name four abuse-case tests.
Expected result and reasoning
Tests such as: delegate cannot approve own claim (403); non-manager cannot create a delegation for another manager (403); approval outside the delegation window is refused (403); a delegated approval writes an audit event naming delegate and delegator. Each record names an owner and a revisit trigger.
6. Decide whether LINDDUN applies
Assess the nightly email against the LINDDUN threat types.
Expected result and reasoning
Yes. Thumbnails in email are data disclosure to the provider and to inboxes outside access control, and employees may not know delegates can see receipts (unawareness). Send a link to the application, which enforces authorization, instead of images, and tell employees about delegation.
Paper exercise using only the stated rules. A real approval policy may differ, for example by forbidding delegation to a direct report; record such choices as explicit design decisions.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- Threat Modeling Manifesto
- OWASP Threat Modeling Cheat Sheet
- Microsoft: Uncover Security Design Flaws Using the STRIDE Approach (MSDN Magazine, 2006). November 2006.
- Mbaka and Tuma: A replication of a controlled experiment with two STRIDE variants (2022)
- Bruce Schneier: Attack Trees (Dr. Dobb's Journal, 1999). December 1999.
- LINDDUN privacy threat types
- SEI: Threat Modeling, A Summary of Available Methods (2018)
- NIST SP 800-218, Secure Software Development Framework 1.1 (PW.1). Version 1.1 (February 2022).
- OWASP Top 10:2025 A06 Insecure Design
- OWASP pytm
Reference information
- Tags
- Threat modeling
- SDLC phase
- Design