Threat Modelling in Practice
Finding a security flaw in a design document costs a meeting. Finding the same flaw after launch costs an incident. Threat modelling is the habit of looking at a system before (and while) it is built and asking, in an organised way, what could go wrong and what you will do about it.
You met the six STRIDE categories in Secure Coding Practices. This lesson turns them into a complete, repeatable process: you will draw a data flow diagram with trust boundaries, apply STRIDE to each element, build an attack tree, rank threats in a register and turn them into fixes that can be tested. The example is a fee-payment system for a coaching centre in Lucknow.
The four questions
The Threat Modeling Manifesto, written by a group of experienced practitioners, describes threat modelling with four questions. Every method, tool and template is a way of answering them:
- What are we working on? A model of the system: usually a diagram.
- What can go wrong? Threats, found with a method such as STRIDE.
- What are we going to do about it? A decision for each threat.
- Did we do a good enough job? A review: were the fixes made, and does the model still match the system?
A threat model does not need special software. A whiteboard photo, a table and a ticket for each fix is a real threat model. What matters is that developers, testers and someone who understands the business answer the questions together.
Step 1: draw a data flow diagram
A data flow diagram (DFD) shows how data moves. It has only five kinds of element:
| Element | Drawn as | Example |
|---|---|---|
| External entity | Rectangle | A student, a payment gateway |
| Process | Circle or rounded box | The Fees API |
| Data store | Two parallel lines | The student database, the log store |
| Data flow | Arrow | "Pay fee" request over HTTPS |
| Trust boundary | Dashed line | Where data crosses from the internet into your network |
Trust boundaries are the most important part. Every arrow that crosses one is a place where an attacker can send data you did not expect, so most threats cluster there.
INTERNET (untrusted) | COACHING CENTRE CLOUD (trusted)
|
[Student app] --1 login/pay---->|--> (Fees API) --3 SQL--> = Student DB =
| |
[Payment gateway] --2 webhook-->|--------+---4 events--> = Audit log =
|
[Office staff] --5 admin login->|--> (Admin panel) --6 SQL--> = Student DB =Number every flow so threats can refer to it ("flow 2: the webhook can be forged"). Keep the diagram at the level of detail where each box has one owner.
Step 2: apply STRIDE to each element
Instead of asking "what about STRIDE?" for the whole system, STRIDE per element asks only the questions that fit each kind of element. A data store cannot log in, so spoofing does not apply to it; a process can suffer all six threats:
STRIDE_PER_ELEMENT = {
"external entity": "SR",
"process": "STRIDE",
"data store": "TID",
"data flow": "TID",
}
NAMES = {"S": "Spoofing", "T": "Tampering", "R": "Repudiation",
"I": "Info disclosure", "D": "Denial of service", "E": "Elevation"}
elements = [
("Payment gateway", "external entity"),
("Fees API", "process"),
("Student DB", "data store"),
("Flow 2: webhook", "data flow"),
]
total = 0
for name, kind in elements:
letters = STRIDE_PER_ELEMENT[kind]
total += len(letters)
print(f"{name:<16} {kind:<16} {', '.join(NAMES[x] for x in letters)}")
print("Questions to answer:", total)Payment gateway external entity Spoofing, Repudiation Fees API process Spoofing, Tampering, Repudiation, Info disclosure, Denial of service, Elevation Student DB data store Tampering, Info disclosure, Denial of service Flow 2: webhook data flow Tampering, Info disclosure, Denial of service Questions to answer: 14
For each question, the team writes a concrete threat or "not applicable, because...". For flow 2, Spoofing of the payment gateway becomes: "An attacker sends a fake 'payment successful' webhook, and a student's fee is marked paid without any money." That sentence is specific enough to fix and to test. (Log stores also face repudiation, because deleting logs lets someone deny an action.)
Step 3: build an attack tree for the scariest goal
An attack tree starts from one attacker goal at the top and breaks it into ways to reach it. An OR node needs any one child; an AND node needs all of them. Giving each leaf a rough effort score shows the attacker's cheapest path, which is where your fix will help most:
tree = ("OR", "Read another student's fee receipt", [
("LEAF", "Change the receipt ID in the API", 1),
("AND", "Use the admin panel", [
("LEAF", "Find the admin panel URL", 2),
("LEAF", "Phish an office staff password", 5),
("LEAF", "Get past MFA", 20),
]),
("LEAF", "Copy a database backup from a shared drive", 8),
])
def cheapest(node):
kind, label, rest = node
if kind == "LEAF":
return rest, [label]
results = [cheapest(child) for child in rest]
if kind == "OR":
return min(results)
return sum(r[0] for r in results), [step for r in results for step in r[1]]
cost, path = cheapest(tree)
print(f"Cheapest path (effort {cost}): {' + '.join(path)}")
tree[2][0] = ("LEAF", "Change the receipt ID in the API (ownership check added)", 999)
cost, path = cheapest(tree)
print(f"After the fix (effort {cost}): {' + '.join(path)}")Cheapest path (effort 1): Change the receipt ID in the API After the fix (effort 8): Copy a database backup from a shared drive
The numbers are rough guesses, and that is fine. The tree shows that MFA makes the admin route expensive, that the missing ownership check (see API Security & OWASP API Top 10) is the urgent problem, and that once it is fixed, the shared-drive backup becomes the next weakest point.
Step 4: decide and record
Each threat gets one of four responses:
- Mitigate: add a control (verify the webhook signature, add MFA).
- Eliminate: remove the feature or data (do not store full card details at all).
- Transfer: let a better-placed party carry it (the payment gateway handles card data).
- Accept: the owner knowingly accepts a low risk, in writing, with a review date.
Record threats in a register that lives next to the code, so it changes when the design changes:
- id: TM-04
element: Flow 2 (payment gateway webhook)
stride: Spoofing
threat: Forged "payment successful" webhook marks a fee as paid
likelihood: 4 # 1-5: webhook URL is easy to find
impact: 4 # 1-5: direct money loss
response: mitigate
control: Verify the gateway's HMAC signature; confirm status with the gateway's API
owner: Kabir (backend)
test: Unit test sends an unsigned webhook and expects 401
status: openThe test line is what makes threat modelling more than paperwork. A threat is closed only when a test, review or scan shows the control works.
register = [
("TM-01", "Spoofed student login via password guessing", 3, 4, "done"),
("TM-02", "Receipt IDs readable by other students", 5, 4, "open"),
("TM-04", "Forged payment webhook", 4, 4, "open"),
("TM-07", "Admin denies changing a fee (no audit log)", 2, 3, "open"),
]
open_items = sorted((t for t in register if t[4] == "open"), key=lambda t: t[2] * t[3], reverse=True)
for tid, threat, likelihood, impact, _ in open_items:
print(f"{tid} risk {likelihood * impact:>2} {threat}")TM-02 risk 20 Receipt IDs readable by other students TM-04 risk 16 Forged payment webhook TM-07 risk 6 Admin denies changing a fee (no audit log)
Step 5: keep it alive
Revisit the model when something important changes: a new integration, a new type of personal data, a move to a new cloud service, or after an incident. A short session per feature works better than one huge workshop per year. Tools such as OWASP Threat Dragon (free, open source) and the Microsoft Threat Modeling Tool draw DFDs and suggest STRIDE threats, but they are only helpers for the conversation.
For privacy threats, such as linking a student's records across systems or collecting more personal data than needed, the LINDDUN framework plays the same role as STRIDE and fits well with the duties of the DPDP Act 2023 (see Ethics & Indian Cyber Law).
Common mistakes
- Modelling after the code is finished, when design changes are expensive.
- Drawing no trust boundaries, so the riskiest data flows are not obvious.
- Generic threats such as "hackers attack the server" that nobody can fix or test.
- No owner and no test for each mitigation, so the register becomes shelfware.
- Doing it alone. The person who wrote the code has the same blind spots as the code.
Key points
- Threat modelling answers 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.
- A data flow diagram has external entities, processes, data stores, data flows and trust boundaries; threats cluster at trust boundaries.
- STRIDE per element asks only the questions that fit each element type.
- Attack trees show the attacker's cheapest path and how a fix changes it.
- Each threat is mitigated, eliminated, transferred or accepted, with an owner and a test.
- Update the model when the system changes; LINDDUN covers privacy threats.
Exercise
Add a new element to the STRIDE example: the "Audit log" data store. Because logs can be deleted to deny actions, give log stores the letters TRID instead of TID. Change the program so that any data store whose name contains "log" uses the log rule, and print the new total number of questions.
Show solution
STRIDE_PER_ELEMENT = {"external entity": "SR", "process": "STRIDE", "data store": "TID", "data flow": "TID"}
elements = [
("Payment gateway", "external entity"),
("Fees API", "process"),
("Student DB", "data store"),
("Flow 2: webhook", "data flow"),
("Audit log", "data store"),
]
total = 0
for name, kind in elements:
letters = STRIDE_PER_ELEMENT[kind]
if kind == "data store" and "log" in name.lower():
letters = "TRID"
total += len(letters)
print(f"{name:<16} {letters}")
print("Questions to answer:", total)The audit log gets four questions, so the total rises from 14 to 18. Repudiation matters for logs because an attacker or insider who can edit them can deny what they did.
Quick Quiz
1. Where do threats usually cluster in a data flow diagram?
Crossing a trust boundary means data comes from a less trusted place and must be checked.
2. Under STRIDE per element, which threats apply to a process?
Processes run code, so they can be spoofed, tampered with, abused and more.
3. In an attack tree, what does an AND node mean?
AND nodes add up the effort of every step; OR nodes take the cheapest child.
4. When is a mitigated threat really closed?
A recorded test turns a threat model into checked security.
Learn Cyber Security with a mentor
Free lessons get you started. Our Ethical Hacking & Cyber Security training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
