CyberNugget

L05 · Threat Modeling with STRIDE

Module 1: Security Foundations · ~20 min · Lesson 5 of 71

🎯 What you'll learn

  • The four questions behind every threat model, and why the first one is a drawing
  • How to draw a data flow diagram (DFD) with trust boundaries for a service you own
  • The six STRIDE categories, and which security property from L01 each one breaks
  • How to turn "what can go wrong?" into a table of threats, mitigations, and tickets

🔥 Why this matters

Picture a team at an insurer about to ship PATCH /v1/policies/{id}. Code review will check that the code works, but nobody in code review asks "can a customer change someone else's policy?" or "what if a pod inside the cluster calls the service directly, skipping Kong?" Those questions are cheap on a whiteboard and expensive in an incident report. The 2022 Optus breach (L44) came down to an internet-facing API that didn't properly check who was calling it: exactly the gap a threat model is built to catch.

1. Four questions

Threat modeling is looking at a design, before or while you build it, to find what could go wrong and decide what to do about it. Adam Shostack, who led threat modeling work at Microsoft, boils it down to four questions:

 1. What are we working on? ─────▶ draw it (a data flow diagram)
          │
          ▼
 2. What can go wrong? ──────────▶ walk the drawing with STRIDE
          │
          ▼
 3. What are we going to do ─────▶ mitigate, eliminate, transfer,
    about it?                      or accept each threat
          │
          ▼
 4. Did we do a good job? ───────▶ every element checked? every
          │                        threat has an owner or ticket?
          │
          └──▶ design changes? go back to 1

An analogy: it's a home inspection done on the blueprints instead of after you've moved in. Technically, it's a structured design review that produces a list of threats, each tied to a part of the system and a decision. A whiteboard, the engineers who know the system, and an hour are enough.

2. Question 1: draw a data flow diagram

You can't find threats in a system you can't see, so step one is a data flow diagram (DFD): a picture of where data comes from, where it goes, and where it's kept. It has five building blocks:

 [ Customer ]      EXTERNAL ENTITY  people or systems you don't
                                    control: users, partners
 ( policy-svc )    PROCESS          code that runs: services,
                                    gateways, batch jobs
 ═══════════════
   MongoDB         DATA STORE       data at rest: databases,
 ═══════════════                    buckets, queues, log files
 ──── F1 ────▶     DATA FLOW        data moving between two of
                                    the above (label each one)
 ─ ─ ─ ─ ─ ─ ─     TRUST BOUNDARY   a line where the level of
                                    trust changes

The most important block is the trust boundary: a line where data passes between parts of the system that have different privileges or are trusted to different degrees. The internet and an API gateway sit on opposite sides of one. So do a service and its database, and a batch job running in another account. A boundary often matches a firewall, but it doesn't have to: it's about who controls what, not about cables.

Almost every serious threat lives where a data flow crosses a trust boundary. On one side, someone you don't fully trust controls the data; on the other, your code acts on it. That crossing is where authentication, validation, and authorization checks belong. Keep the DFD coarse: one box per service, not per class.

3. Question 2: what can go wrong? STRIDE

Asking "what could go wrong?" at a diagram tends to produce silence, or one person's favorite worry. STRIDE, created at Microsoft by Loren Kohnfelder and Praerit Garg in 1999, gives you six prompts instead. Each one is the opposite of a security property you met in L01:

Letter Threat Property it violates Plain-language question
S Spoofing Authenticity Can someone pretend to be a user or a service?
T Tampering Integrity Can someone change data in transit or at rest?
R Repudiation Non-repudiation Can someone do something and later deny it?
I Information disclosure Confidentiality Can someone read data they shouldn't?
D Denial of service Availability Can someone make it slow or unusable?
E Elevation of privilege Authorization Can someone do more than they're allowed to?

In L01 you used these properties to say what failed after an incident; STRIDE asks the same questions before one. Authorization wasn't one of L01's named properties, but you saw it in the log examples: it's the rule about who may do what, and elevation of privilege breaks it.

You don't need all six questions for every element. STRIDE per element narrows them by type:

Element Threats that usually apply
External entity S, R
Process S, T, R, I, D, E (all six)
Data store T, I, D (plus R if it holds logs)
Data flow T, I, D

Walk the diagram element by element, asking only the questions on its row, and spend most of your time on elements that touch a trust boundary.

Question 3: what are we going to do about it?

Each threat gets one of four decisions. Mitigate it with a control. Eliminate it by removing the feature or the data. Transfer it to someone else, such as an identity provider or an insurer. Or accept it, on purpose, with a named owner, because the risk (likelihood × impact, from L02) is low enough. "We didn't get to it" is not a decision.

🧪 Worked example: threat-modeling /v1/policies/{id}

Q1: What are we working on? Customers read their policy (GET) and update contact details (PATCH) from a mobile app. Requests go through Kong to policy-service on EKS, which uses a MongoDB policies collection. A nightly Airflow job, running in its own environment outside the cluster, bulk-upserts data from an upstream admin system into the same collection.

   [ Customer: mobile app ]
            │ F1  GET/PATCH /v1/policies/{id}
            │     HTTPS + OAuth access token
 ─ ─ ─ ─ ─ ─│─ ─ TB1: internet │ company edge ─ ─ ─ ─ ─ ─ ─ ─ ─
            ▼
     ( Kong gateway )
            │ F2  request + X-Consumer-ID header, mTLS
 ─ ─ ─ ─ ─ ─│─ ─ TB2: edge │ EKS ─ ─ ─ ─
            ▼
     ( policy-service ) ──F4 audit event──▶ ═══ audit log ═══
            │
            │ F3  find / updateOne          ┌ ─ TB4: Airflow env ─ ┐
            │     (service DB user)            ( Airflow DAG )
            │                               └ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─┘
            │                                        │ F5 nightly
            │                                        │    upsert
 ─ ─ ─ ─ ─ ─│─ ─ TB3: app │ data tier ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─
            ▼                                        ▼
   ═══════════════════════════════════════════════════════
           MongoDB · policies collection (data store)
   ═══════════════════════════════════════════════════════

Q2 and Q3: What can go wrong, and what will we do? STRIDE per element, focusing on the boundary crossings (made-up IDs):

# Where STRIDE Threat Decision
1 F1 S A stolen or forged token is replayed Mitigate: Kong checks JWT signature, issuer, audience, expiry; short-lived tokens (L25)
2 F2 S A pod inside the cluster calls policy-service directly with X-Consumer-ID: C-20417 Mitigate: network policy so only Kong can reach it; service verifies the token itself (L43, L51)
3 policy-service I GET /v1/policies/PL-100246 returns another customer's policy Mitigate: check the policy's owner against the token (L34, L40)
4 policy-service E A PATCH body sneaks in "ownerId" or "status":"PAID_UP" Mitigate: allow-list of patchable fields (L41)
5 F3 T {id} arrives as {"$ne": null} and matches any document Mitigate: accept only strings matching ^PL-\d{6}$ (L31)
6 Customer, F4 R "I never changed my address," after a fraud claim Mitigate: audit caller, old/new values, correlation ID to an append-only store (L60)
7 Kong D One client floods GETs or sends a 50 MB PATCH body Mitigate: per-consumer rate and size limits (L42)
8 F5 T, D A bad upstream file overwrites 40,000 live policies Mitigate: row-count and schema checks first; tested point-in-time restore
9 Airflow DAG E The DAG's DB user has root, so anyone who can merge a DAG can drop the database Mitigate: custom role writing only to policies; password in a vault (L48, L53)
10 Audit log I Logs include full request bodies with tax IDs Accept for this release: owner platform team, fix by Q1 (logs are encrypted, access is narrow)

Notice rows 3 and 4. Kong knows who is calling, but only policy-service knows which policies belong to them. A gateway can't fix those threats, and that's an insight the diagram gives you before you read any code.

Q4: Did we do a good job? Every element and every flow across TB1 to TB4 was asked its STRIDE questions, and every row has a ticket or a named owner. Where you can, turn mitigations into tests: GET on someone else's policy returns 404, and a PATCH with ownerId is rejected. Keep the diagram and table in the repo and update them when the design changes.

🛠 Try it (optional, ~5 min)

Run STRIDE on a login endpoint, POST /v1/auth/login, which takes a username and password and returns a session token. Sketch it first (customer → Kong → auth-service → user store), then write one threat and one mitigation per letter.

Possible answers, don't peek:

  • S credential stuffing with leaked passwords → MFA, rate limits, breached-password checks (L18, L21)
  • T a redirect parameter changed to an attacker's site → allow-list redirect URLs
  • R failed logins aren't logged → log each attempt's time, IP, and outcome (never the password)
  • I "user not found" vs "wrong password" reveals valid usernames → one generic error
  • D attackers lock out real users on purpose (the L01 trade-off) → throttle instead of locking
  • E the service trusts a "role" field in the request → roles come only from the user store

🧠 Key takeaways

  • Threat modeling answers four questions: what are we working on, what can go wrong, what will we do about it, and did we do a good job.
  • Start with a DFD: external entities, processes, data stores, data flows, and trust boundaries.
  • Most serious threats sit where a data flow crosses a trust boundary, so put checks there.
  • STRIDE is the inverse of L01's properties: S↔authenticity, T↔integrity, R↔non-repudiation, I↔confidentiality, D↔availability, E↔authorization.
  • Every threat ends with a decision (mitigate, eliminate, transfer, accept) and an owner.

📖 Words to know

Term Meaning
Threat modeling Reviewing a design to find what could go wrong and decide what to do about each threat
Data flow diagram (DFD) A drawing of a system's external entities, processes, data stores, and data flows
External entity A person or system outside your control that sends or receives data
Process Running code that handles data: a service, gateway, or job
Data store Somewhere data rests: a database, bucket, queue, or log
Data flow Data moving between two elements, drawn as a labeled arrow
Trust boundary A line where data passes between parts of a system with different levels of trust
STRIDE Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege
STRIDE per element Applying only the STRIDE categories that fit each DFD element type
Risk acceptance A deliberate, owned decision to live with a threat instead of mitigating it

⏭ Next time

L06: the Equifax breach of 2017, where an unpatched server, a flat network, and an expired certificate turned one known vulnerability into one of the largest data breaches in US history.

Deep dive 1

Threat modeling in a sprint (a 30-minute format)

The problem with "big" threat models

Many teams picture threat modeling as a two-day workshop with a security architect, a 40-page document, and a diagram nobody updates. That kind of model is out of date by the next release. The alternative that works on agile teams is small and frequent: a short session on the change, not the whole system, run by the engineers building it.

When to do one

You don't threat-model every story. Use a short list of triggers in backlog refinement. If a story does any of these, it gets a session:

  • Adds a new trust boundary crossing: a new public endpoint, a new partner integration, a new webhook, a new caller of an internal service
  • Handles a new kind of sensitive data, such as SSNs, bank details, or health information
  • Changes authentication or authorization: new roles, new token scopes, a new identity provider
  • Adds a new data store or a new writer to an existing one (like the Airflow job in the main lesson)
  • Changes who can deploy or run something: CI permissions, IAM roles, DB users

A copy change on a screen, or a refactor that keeps the same flows, doesn't need one.

The 30-minute agenda

 0 min       5            20            25           30
 │  DRAW     │  STRIDE     │  DECIDE     │  RECORD    │
 │  the      │  only the   │  rank by    │  tickets,  │
 │  change   │  elements   │  likelihood │  accepted  │
 │  on the   │  the change │  × impact;  │  risks,    │
 │  DFD      │  touches    │  pick an    │  tests     │
 │           │             │  owner      │            │
 └─── Q1 ────┴──── Q2 ─────┴──── Q3 ─────┴──── Q4 ────┘

Who: the developer building it, the tech lead, and whoever knows the upstream and downstream systems. A security engineer is welcome but shouldn't be required, or sessions won't happen.

Rules that keep it to 30 minutes: start from the existing DFD and draw only what's new (in a different color). Ask STRIDE only of elements the change touches, using the per-element table. Don't design fixes in the room; write the threat, the decision, and an owner, and design the fix in the ticket. If you run out of time, schedule a second session rather than skipping step 4.

🧪 Worked example: "add a partner webhook"

Story: "When a policy's address changes, POST a notification to a partner's URL." The trigger list fires twice: a new flow crosses a trust boundary outward, and the partner is a new external entity.

 ( policy-service ) ──F7 POST event (HTTPS)──▶ [ Partner system ]
                       ─ ─ ─ ─ TB5 ─ ─ ─ ─

Ten minutes of STRIDE on F7 and the partner produce:

STRIDE Threat Decision
S Someone posts fake events to the partner, pretending to be us Mitigate: HMAC-sign the body with a shared secret per partner (L17)
I The payload includes the full new address, more than the partner needs Eliminate: send only the policy ID and "address changed"; partner fetches details through its own authorized API
T The partner URL is editable in config, so anyone with config access can point it at their own server Mitigate: URL changes need a second approver; allow-list partner domains
D The partner is slow, and synchronous calls back up policy-service Mitigate: send through a queue with retries and a timeout
E The partner URL is set to http://169.254.169.254/... Mitigate: block internal and metadata addresses for outbound calls (SSRF, L35)

The output is five tickets and one changed requirement (the slimmer payload), found before a line of webhook code was written.

Keep the model next to the code

Save the DFD and the threat table in the repo, for example docs/threat-model.md, so pull requests can update it. Some teams go further with threat modeling as code: tools like OWASP pytm let you describe the system in Python and generate the diagram and a first list of threats. Free GUI tools include OWASP Threat Dragon and the Microsoft Threat Modeling Tool. The tool matters far less than the habit.

🛠 Try it (~5 min)

Look at your team's current sprint board. Which stories hit at least one trigger from the list above? For one of them, write down the new element or flow it adds to your service's DFD, and the one STRIDE letter you think matters most for it.

🧠 Extra takeaways

  • Threat-model the change, not the whole system, and do it often.
  • Use triggers (new boundary crossing, new sensitive data, auth changes, new data store or writer) to decide which stories need a session.
  • Timebox it: draw, STRIDE, decide, record. Design fixes in tickets, not in the room.
  • Keep the model in the repo so it changes along with the code.
Term Meaning
Threat modeling trigger A kind of change that always gets a threat-modeling session
Threat modeling as code Describing a system in code so diagrams and candidate threats are generated and versioned
OWASP pytm A Python framework for threat modeling as code
OWASP Threat Dragon A free, open-source tool for drawing DFDs and recording threats
Deep dive 2

Beyond STRIDE (attack trees, PASTA, LINDDUN)

STRIDE isn't the only lens

STRIDE is a great checklist for "what kinds of things go wrong with this element?" It's weaker at two other questions: "what are the different ways an attacker reaches one specific goal?" and "what privacy harm could this do even if nobody breaks in?" Other methods answer those. You won't use them daily, but you'll see the names in security reviews.

Attack trees: many paths to one goal

An attack tree puts one attacker goal at the root and breaks it into the ways to achieve it. Children of an OR node are alternatives (any one works); children of an AND node must all happen. Bruce Schneier popularized the idea in 1999. Here's a tree for the threat that matters most in the worked example:

 GOAL: change another customer's beneficiary
 │
 ├─ OR ─ 1. act as that customer
 │       ├─ 1a. phish their password + MFA code       (L66)
 │       ├─ 1b. credential stuffing, no MFA on login   (L18)
 │       └─ 1c. steal a token from a logged device
 │
 ├─ OR ─ 2. use your own account against their policy
 │       ├─ 2a. PATCH /v1/policies/{their id}: BOLA   (L40)
 │       └─ 2b. send "ownerId" in a PATCH: mass
 │              assignment                              (L41)
 │
 └─ OR ─ 3. go around the API
         └─ AND ─ 3a. get the Airflow DB credentials
                  3b. reach MongoDB from the network
                  3c. update the document directly

The tree makes priorities visible. Path 2 needs only a normal account and a curl command, so it's the cheapest for an attacker and should be fixed first. Path 3 is an AND: blocking any one step (say, the network route in 3b) cuts the whole branch. Teams often annotate each leaf with cost, skill, or likelihood to compare paths.

PASTA: risk-centric and heavier

PASTA (Process for Attack Simulation and Threat Analysis), published by Tony UcedaVélez and Marco Morana in 2015, is a seven-stage method that starts from business objectives rather than the diagram:

 1 business objectives → 2 technical scope → 3 decompose the app
 → 4 threat analysis → 5 vulnerability analysis → 6 attack modeling
 → 7 risk and impact analysis

It ties each threat to business impact, which makes it popular with risk and compliance teams in regulated industries. The cost is effort: it's a multi-week exercise for a major system, not a 30-minute sprint session.

LINDDUN: privacy threats

LINDDUN, developed at KU Leuven in Belgium, is STRIDE's counterpart for privacy. Its categories (as named in recent versions) are:

Letter Threat Example in a policy API
L Linking Joining claims data and app analytics by device ID to build a profile
I Identifying "Anonymized" exports that still contain birth date + ZIP code
N Non-repudiation Keeping proof of an action the person wanted to be deniable
D Detecting 403 for "exists, not yours" vs 404 for "no such policy" reveals that a record exists
D Data disclosure Sending more personal data to a partner than the purpose needs
U Unawareness and unintervenability Customers can't see or correct what's stored about them
N Non-compliance Keeping data longer than the retention policy allows

Notice the twist: in STRIDE, non-repudiation is a goal you protect. In LINDDUN, it can be a threat, because some actions (like anonymous whistleblowing) should be deniable. Same property, different point of view. Privacy gets its own lesson in L69.

Which one when?

Method Best at Effort
STRIDE Systematic coverage of a design, element by element Low to medium
Attack tree Comparing paths to one high-value goal Low per tree
PASTA Linking threats to business risk for a major system High
LINDDUN Privacy harms, including ones with no attacker Medium

They combine well: STRIDE to find threats, an attack tree for the scariest one, LINDDUN for any flow carrying personal data.

🛠 Try it (~5 min)

Build an attack tree for "read another customer's policy details" in the worked example's system. Aim for at least three OR branches and one AND branch. Mark the cheapest path for an attacker, then check whether the main lesson's threat table already mitigates it.

🧠 Extra takeaways

  • Attack trees show the different ways to reach one goal; cut an AND branch anywhere to block it.
  • PASTA starts from business objectives and ends in risk analysis; thorough but heavy.
  • LINDDUN covers privacy, where even legitimate processing can cause harm.
  • Use STRIDE by default and add the others where they answer a question STRIDE can't.
Term Meaning
Attack tree A tree with an attacker goal at the root and alternative (OR) or combined (AND) steps below it
PASTA A seven-stage, risk-centric threat modeling method that starts from business objectives
LINDDUN A privacy threat modeling framework: Linking, Identifying, Non-repudiation, Detecting, Data disclosure, Unawareness, Non-compliance

The quiz for this lesson and the Tests section need JavaScript.