Threat Model Example: A Complete Walkthrough for a SaaS Web App

Most explanations of threat modeling stop at the theory: draw a diagram, apply STRIDE, prioritise. It sounds simple until you sit down with a real system and wonder what "apply STRIDE" actually produces, how detailed the diagram should be, or what a finished threat model looks like.

So this post works through one, start to finish, for a realistic system: Invoicely, a small SaaS app that lets businesses create invoices, email them as PDFs and get paid by card. It's fictional, but every part of it is common: a web API, a database, file storage, a background worker, a payment provider, an email provider and an internal admin console. Each step shows the actual artefact, not just a description of it, and the finished model is free to open in ThreatTree.

The short version: scope the system and its assets, draw a data flow diagram with trust boundaries, walk every element through STRIDE, decompose the most serious threat into an attack tree, score each step on likelihood and impact, and record every result in a risk register with a decision and an owner. For Invoicely that took about two hours with three people, and the most important finding was one nobody had thought of before the diagram existed.

Step 1: Scope and assets

Before any diagram, agree on what's being modeled and what matters. For this exercise the scope is the production Invoicely service: everything from the customer's browser to the database, including the support team's admin console. Out of scope: Stripe's and the email provider's internal systems (we model our integrations with them, not their infrastructure), and the marketing website.

AssetWhy it matters
Invoices and customer recordsCommercially sensitive: amounts, clients, addresses. A leak between customers would end the business.
Invoice PDFsThe same data, in a form that's easy to forward and hard to revoke.
Payment statusIf it can be faked, customers stop trusting that "paid" means paid.
User accounts and sessionsThe keys to everything above.
Availability at month endMost invoices go out in the last three days of the month.

The realistic threat actors are an external attacker after customer data, a malicious or curious customer probing other customers' invoices, a support agent misusing their access, and an opportunist with stolen credentials. That's a short list, and it's enough to keep the rest of the exercise grounded.

Step 2: The data flow diagram

Next, draw the system as a data flow diagram: external entities (rectangles), processes (ovals), data stores (parallel lines), data flows (arrows) and trust boundaries (dashed boxes). The goal isn't a perfect architecture diagram. It's a shared, accurate picture of where data goes and where trust changes. Our guide to building a DFD covers the drawing itself.

Data flow diagram of Invoicely: a customer's browser and a support agent outside the trust boundary; inside the Invoicely cloud account, a Web API, an Admin console and a PDF worker; a data tier boundary containing Postgres (customers, invoices) and file storage (invoice PDFs); and Stripe and an email provider as external services. Flows include HTTPS login and invoices, session cookie and data, SSO login and lookups, create payment, a payment webhook, render job, send invoice, SQL, and write PDF. A dashed line labelled shared DB role connects the Web API and the Admin console.
Invoicely's data flow diagram. Note the dashed "shared DB role" line: drawing it is what surfaced the most serious finding below.

Two things came out of drawing it, before anyone had said the word "threat". First, the admin console and the customer API use the same database role, so the console can technically read and change anything. Second, invoice PDFs are written to file storage and emailed as links, so whoever has the link can open the file. Both look fine in an architecture slide. Both look very different once you draw the trust boundaries.

Step 3: The STRIDE sweep

Now take each element and flow in turn and ask the six STRIDE questions: can something be spoofed, tampered with, repudiated, disclosed, denied or used to escalate privilege? Start with anything that crosses a trust boundary. Our threat modeling checklist breaks each question into concrete prompts. Write each finding as a threat statement: [actor] can [action] because [weakness], resulting in [impact].

#ElementSTRIDEThreat
T1Web APISpoofingAn attacker can take over customer accounts with leaked passwords because there's no rate limit or breached-password check on login.
T2Web APIInfo disclosureA customer can read another customer's invoices by changing the invoice ID in the API, because ownership is checked in the UI but not on every endpoint.
T3File storageInfo disclosureAnyone with a PDF link can open the invoice because links don't expire and aren't tied to an account.
T4Stripe webhookTamperingAn attacker can mark invoices as paid by sending a forged webhook if the signature isn't verified.
T5Admin consoleElevationA support agent can read or change any customer's data because the console shares the API's database role.
T6Admin consoleRepudiationA support agent can look up or edit a customer record and deny it, because console actions aren't logged.
T7Web APITamperingAn attacker can run script in a customer's session via stored XSS in an invoice note, because notes are rendered as HTML.
T8Browser sessionSpoofingScript running in the page can steal the session token because it's stored in localStorage rather than an httpOnly cookie.
T9PDF workerDenial of serviceA customer can stall invoice sending for everyone by queuing thousands of renders, because there's no per-account limit.
T10Email flowInfo disclosureInvoice emails sent to a mistyped address expose the full invoice, because the PDF link works for anyone.

Ten threats from one sweep is typical for a system this size. Several categories were "not applicable" for some elements, and saying so quickly kept the session moving. Note that T3 and T10 share a root cause (the open PDF link), and T7 and T8 only matter together, which is exactly what the next step is for.

Step 4: An attack tree for the threat that matters most

A flat list tells you what could go wrong. It doesn't show how threats combine, or which single fix closes the most paths. For that, pick the attacker goal that worries the business most and decompose it. For Invoicely, that's clearly "read another customer's invoices". OR gates are alternatives (any one path works); AND gates are steps that must all succeed.

Attack tree for the goal Read another customer's invoices. An OR gate leads to four paths: change the invoice ID in the API (IDOR), scored likelihood 4 times impact 5 equals 20, Critical; guess a PDF link in file storage, 2 times 5 equals 10, High; abuse a support agent account, which splits through an OR gate into phishing the agent's SSO password (3 times 4 equals 12, High) and an insider looking up a competitor (2 times 4 equals 8, Medium); and steal a session via XSS, which needs both, through an AND gate, a stored XSS in an invoice note (2 times 4 equals 8, Medium) and a session token readable by JavaScript (3 times 4 equals 12, High).
The attack tree for Invoicely's most serious goal, with each step scored as likelihood × impact.

Three things stand out. The IDOR path (T2) needs no special skill, just a logged-in account and a browser's developer tools, so it scores highest. The support-agent paths exist only because of the shared database role (T5): fix that and the insider path shrinks to what the agent's role legitimately allows. And the XSS path is an AND: it needs both the stored XSS and a token readable by JavaScript, so moving the session into an httpOnly cookie breaks that whole branch even before the XSS is fixed.

Step 5: Score every threat

Score each threat on likelihood and impact from 1 to 5, and multiply. Use the context of this system rather than a generic severity: a missing rate limit matters more on a login page than on a settings page. Our post on CVSS versus threat model risk explains why. A simple banding keeps the conversation short: 1–4 Low, 5–9 Medium, 10–15 High, 16–25 Critical. Our risk matrix guide plots these ten threats on a 5×5 matrix and gives likelihood and impact scales to score against.

#Threat (short)LIScore
T2IDOR on invoice endpoints4520 Critical
T1Credential stuffing on login4416 Critical
T5Admin console's shared DB role3515 High
T8Session token readable by JS3412 High
T4Forged payment webhook2510 High
T3Non-expiring PDF links2510 High
T9Render queue flooding339 Medium
T7Stored XSS in invoice notes248 Medium
T6No audit log in admin console326 Medium
T10Mistyped email exposes invoice236 Medium

Step 6: The risk register

Scoring isn't the end. Each threat needs a treatment (mitigate, avoid, transfer or accept), an owner who answers for it, and the work as tickets. Accepting a risk is a legitimate decision, as long as someone with authority makes it and it has a review date; our post on why risk registers turn into backlogs explains what goes wrong otherwise. Here are the top entries for Invoicely:

#TreatmentActionOwner
T2MitigateEnforce ownership checks in a shared authorisation layer for every invoice and PDF endpoint; add tests that call each endpoint as another tenant.Engineering lead
T1MitigateRate-limit logins, block breached passwords, and offer authenticator-app two-factor.Engineering lead
T5MitigateGive the admin console its own database role with read-only access to the fields support needs; route edits through the API.Head of support + platform
T8MitigateMove the session into an httpOnly, SameSite cookie. Breaks the XSS branch of the attack tree on its own.Engineering lead
T3 / T10MitigateReplace open PDF links with short-lived signed URLs that require sign-in to regenerate.Product + engineering
T4MitigateVerify Stripe webhook signatures and reject replays (already supported by Stripe's libraries).Engineering lead
T10Accept (residual)After signed URLs, the remaining risk of a mistyped address is accepted; reviewed in six months or if email delivery changes.CEO

Our guide to writing mitigations auditors accept covers how specific each action should be, and prioritising a backlog of threats covers what to do when the list is longer than this one.

What made this threat model useful

  • The diagram did half the work. The two most important issues, the shared database role and the open PDF links, became obvious the moment the trust boundaries were drawn.
  • The attack tree changed the order of work. Without it, closing the session-theft path looked like it meant hunting down every XSS bug. With it, the httpOnly cookie, a small change, turned out to break that whole branch.
  • Every threat ended up with an owner. A list of ten problems is a worry. Ten entries with treatments and owners is a plan.
  • It has a re-review trigger. Invoicely plans to add bank-transfer payouts next quarter, a new flow across a trust boundary, so the model gets revisited then; see when to re-threat-model.

Frequently asked questions

What does a finished threat model contain?

A scope and list of assets, a data flow diagram with trust boundaries, the threats found (usually through STRIDE), any attack trees for the most serious goals, a likelihood and impact score for each threat, and a risk register entry for each with a treatment decision and an owner.

How long does a threat model like this take?

For a system the size of Invoicely, about two hours with three people: thirty minutes on scope and the diagram, an hour on the STRIDE sweep, and thirty minutes on the attack tree, scoring and owners. Larger systems are best split into several sessions.

Do I need an attack tree for every threat?

No. Build attack trees for the one or two goals that worry the business most. They show how threats combine and which single fix closes the most paths, which a flat list can't.

How often should the threat model be updated?

Whenever the system changes in a way that affects trust boundaries or data: a new integration, a new type of data, a change to authentication. Review it at least once a year even without such a trigger.

Want to try this on your own system? The free templates include a data flow diagram, an attack tree and a STRIDE risk register you can fill in. For how this example maps to compliance, see threat models as SOC 2 evidence.

Open this threat model in ThreatTree

The whole Invoicely model is in ThreatTree as a free example: the data flow diagram, both attack trees and all ten scored risks with their treatments and owners. Explore it, change it, or use it as the pattern for your own system. When you draw your own DFD, the attack tree generator can draft its first attack trees from the components you've drawn.

Or start a blank model →

Not ready to sign up? Get new threat-modeling guides by email instead.