Threat Modeling with STRIDE
On this page
Design It Security-First, Not After It Breaks#
Threat modeling is structured thinking about what can go wrong with a system before it goes wrong. You describe what you are building, list the ways an attacker could abuse it, decide what to do about each one, and check the result. An hour at the whiteboard during design can remove a flaw that would otherwise surface as a pentest finding, or an incident, after launch, when fixing it means rearchitecting.
A useful framing is four questions, popularised by Adam Shostack:
- What are we working on? Draw the system.
- What can go wrong? Enumerate threats, for example with STRIDE.
- What are we going to do about it? Mitigate, accept, transfer or eliminate each threat.
- Did we do a good job? Check the model against the built system and against incidents.
Step 1: Draw a Data Flow Diagram#
A Data Flow Diagram (DFD) has four element types: external entities (users, partner APIs), processes (your services), data stores (databases, buckets, queues) and data flows (arrows). The crucial addition is the trust boundary: a line wherever the level of trust changes, such as between the internet and your load balancer, between your API and a third-party service, or between a web tier and the database network.
Take a concrete example, a document upload feature:
[Browser] --(1) HTTPS upload--> | [API service] --(2) put object--> [(Object storage)]
| |
trust boundary: internet +--(3) enqueue job--> [(Queue)] --(4)--> [Thumbnail worker]
| |
[Browser] <--(6) signed URL---- | [API service] <--(5) metadata ------------------ +
|
[Partner webhook] --(7) status callback--> [API service]
The drawing can be crude; what matters is that every flow is numbered and every boundary is explicit. Flows that cross a boundary (1, 6 and 7 here) are where attacker-controlled data enters, and they get the most attention. Anything that carries that data into a dangerous sink, such as a query, shell command, file path, HTML page or outbound URL, needs a control.
Step 2: Apply STRIDE#
STRIDE is a mnemonic for six threat categories, each the violation of a security property:
| Threat | Property violated | Typical defence |
|---|---|---|
| Spoofing | Authentication | Strong authentication, signed tokens, mTLS |
| Tampering | Integrity | Input validation, signatures, MACs, access control on stores |
| Repudiation | Non-repudiation | Tamper-evident audit logs tied to identities |
| Information disclosure | Confidentiality | Encryption, least privilege, error hygiene |
| Denial of service | Availability | Quotas, rate limits, size limits, timeouts |
| Elevation of privilege | Authorisation | Server-side authorisation checks, sandboxing |
Not every category applies to every element. A common shortcut ("STRIDE per element") is that external entities are mainly subject to spoofing and repudiation, data flows to tampering, disclosure and DoS, data stores to tampering, disclosure, DoS and repudiation (for log stores), and processes to all six.
Walking the upload example produces a list like this:
| Flow / element | STRIDE | Threat | Mitigation |
|---|---|---|---|
| (1) Upload | T | Malicious file (script, polyglot, zip bomb) | Size limits, type validation by content, store outside web root |
| (1) Upload | D | Huge or endless uploads exhaust workers | Max body size, per-user quota, timeouts |
| (2) Object storage | I | Another user reads the file by guessing the key | Random keys, private bucket, per-request authorisation (IDOR) |
| (4) Worker | E | Image library bug gives code execution | Sandboxed worker, no credentials beyond its bucket, patched libraries |
| (6) Signed URL | I | Long-lived URL shared or leaked | Short expiry, bound to the object |
| (7) Webhook | S | Anyone can POST a fake "scan passed" status | HMAC-signed callbacks, verify signature and timestamp |
| API | R | User denies deleting a document | Audit log with user ID, time, object ID |
| Worker fetches a remote URL for thumbnails | I/E | Worker used to reach internal services | Block internal ranges (SSRF) |
This table is the real output of a threat model. Each row becomes a design decision, a ticket, or a documented risk acceptance.
Making It Repeatable: Threats as Code#
Teams that model many services often keep the model next to the code so it is reviewed like any other change. OWASP pytm does this properly; a minimal sketch shows the idea of generating candidate threats from the flows:
from dataclasses import dataclass
@dataclass
class Flow:
id: int
src: str
dst: str
crosses_boundary: bool
carries: set # e.g. {"user_input", "credentials", "pii"}
authenticated: bool
encrypted: bool
FLOWS = [
Flow(1, "Browser", "API", True, {"user_input", "file"}, True, True),
Flow(7, "Partner", "API", True, {"user_input"}, False, True),
Flow(3, "API", "Queue", False, {"file_ref"}, True, False),
]
def candidate_threats(f: Flow):
if f.crosses_boundary and not f.authenticated:
yield "S", "unauthenticated caller across a trust boundary"
if f.crosses_boundary and "user_input" in f.carries:
yield "T", "attacker-controlled input: validate before it reaches a sink"
if not f.encrypted and f.carries & {"credentials", "pii", "file"}:
yield "I", "sensitive data in cleartext on the wire"
if f.crosses_boundary:
yield "D", "no stated rate or size limit"
for f in FLOWS:
for cat, text in candidate_threats(f):
print(f"flow {f.id} {f.src}->{f.dst} [{cat}] {text}")
Output:
flow 1 Browser->API [T] attacker-controlled input: validate before it reaches a sink
flow 1 Browser->API [D] no stated rate or size limit
flow 7 Partner->API [S] unauthenticated caller across a trust boundary
flow 7 Partner->API [T] attacker-controlled input: validate before it reaches a sink
flow 7 Partner->API [D] no stated rate or size limit
The generated list is a prompt, not an answer. Automation is good at "you forgot to state an auth mechanism for flow 7"; people are needed for "the partner could replay an old callback" or business abuse cases.
Step 3: Attack Trees and Prioritisation#
For the highest-value goals, go deeper with an attack tree. The root is the attacker's goal; children are alternative ways to reach it:
Goal: read another customer's documents
├── Steal their session
│ ├── XSS in document preview
│ └── Session token in a leaked URL
├── Bypass authorisation
│ ├── Guess or enumerate object keys (IDOR)
│ └── Reuse a signed URL after expiry is meant to apply
├── Abuse the worker
│ └── SSRF from thumbnail fetcher to internal metadata / storage API
└── Compromise support tooling
└── Phish a support agent with document access
Trees make prioritisation visible. Score each leaf roughly by likelihood (how easy, how exposed) and impact (how many users, how sensitive), and spend effort on the high-high cells first. Precise numbers are false comfort; a three-level high/medium/low scale that the team agrees on is enough. The support-agent branch is a reminder that threats include people and processes, not just code; see business logic flaws for abuse cases that no data flow diagram shows directly.
Step 4: Close the Loop#
- Track mitigations as work items linked to the model, and record accepted risks with an owner and a review date.
- Hand the model to testers. A penetration test scoped around your trust boundaries finds more than one scoped around a URL list.
- Feed detection. Threats you accept or only partly mitigate are exactly what monitoring should watch for.
- Update after incidents. Every real incident is a branch missing from an old tree; add it.
Other Frameworks, Briefly#
- PASTA is a risk-centric, seven-stage process that ties threats to business impact; heavier, suited to large programmes.
- LINDDUN focuses on privacy threats (linkability, identifiability, and so on), useful where personal data dominates.
- Attack libraries such as MITRE ATT&CK and CAPEC help you check you have not missed known techniques.
STRIDE plus a DFD remains the most common starting point because a development team can learn it in an afternoon.
Common Failure Modes#
- Modelling once and filing it. The model must change when the architecture does: new service, new data store, new third party.
- Too much detail. A diagram with 80 boxes never gets reviewed. Model one feature or service at a time.
- Only the security team attends. The engineers who build the system know where the shortcuts are.
- Listing threats without decisions. A threat with no mitigation, owner or acceptance is just worry.
Practice#
Pick one feature you work on, such as password reset or file export. Draw the DFD with trust boundaries in ten minutes, then write at least one STRIDE threat per boundary-crossing flow, with a mitigation for each. Check whether each mitigation actually exists in the code today.
Check your understanding
4 quick questions on this lesson. Get them all right and the lesson is marked complete.