Most security bugs happen because nobody asked "what could go wrong here?" before the code was written. Threat modeling is the structured habit of asking that question early, when the fix costs a whiteboard sketch instead of an emergency release.
In this lesson you will learn the four questions every threat model answers, how to draw a data flow diagram with trust boundaries, how to enumerate threats with STRIDE, and how to turn results into tickets.
A threat model needs no special tool. It needs four questions answered honestly:
The output is a short living document (docs/threat-model.md) updated in the same pull request that changes the architecture.
A data flow diagram (DFD) uses four shapes: external entities (users, third parties), processes (your API), data stores (database, cache), and arrows between them. Then add trust boundaries wherever data crosses from one level of trust to another.
[Browser] --HTTPS--> | [API server] --TCP--> | [PostgreSQL]
untrusted | trusted | internal
| |
| +--HTTPS--> | [Payment gateway]
| | third partyEvery arrow crossing a boundary is a place where input must be validated, identity checked, and secrets protected. This sketch has three such crossings.
STRIDE lists six ways a system can be attacked. Walk through it for every element of the diagram.
| Threat | Question to ask | Typical control | |---|---|---| | Spoofing | Can someone impersonate a user or service? | Authentication, MFA, signed tokens | | Tampering | Can data be modified in transit or at rest? | TLS, server-side validation | | Repudiation | Can a user deny an action? | Audit logs with identity and time | | Information disclosure | Can data leak to the wrong party? | Encryption, access control, terse errors | | Denial of service | Can the system be made unavailable? | Rate limiting, quotas, timeouts | | Elevation of privilege | Can a user gain extra rights? | Authorization on every request |
Applied to the "Browser to API" arrow this yields concrete findings: a stolen cookie (S), a modified price field (T), no record of who cancelled an order (R), a stack trace in a 500 response (I), an unpaginated search endpoint (D), an /admin route without a role check (E).
You will find more threats than one sprint can fix. Rate likelihood and impact from 1 to 3 and multiply:
const findings = [
{ threat: "Admin route without role check", l: 3, i: 3 },
{ threat: "Stack trace in error page", l: 3, i: 2 },
{ threat: "No audit log for refunds", l: 1, i: 2 },
].map(f => ({ ...f, score: f.l * f.i }))
.sort((a, b) => b.score - a.score);
console.table(findings); // scores 9, 6, 2For each finding record one decision: mitigate (add a control), eliminate (remove the feature or data), transfer (a payment provider stores card numbers instead of you), or accept (document why the risk is tolerable). Then convert every "mitigate" into a ticket with an acceptance test:
### Ticket: Enforce admin role on /admin/* routes
Threat: Elevation of privilege (STRIDE-E), score 9
Control: requireRole("admin") middleware on the admin router
Acceptance test: GET /admin/users as a normal user returns 403The acceptance test makes CI verify the control forever.
Which STRIDE category describes a user modifying the `price` field in a checkout request?
Next lesson: Social Engineering Defenses for Developers — how attackers target engineers specifically, and the habits that stop them.