STRIDE Threat Modeling Walkthrough: A Real Web App Example
A hands-on walkthrough of STRIDE threat modeling applied to a real-world web application — including data flow diagrams, trust boundaries, and how to turn findings into actionable engineering tasks.
Why Most Teams Skip Threat Modeling (And Why That's a Mistake)
Threat modeling has a reputation for being slow, academic, and disconnected from how engineers actually build software. I've heard every version of the objection: "We move too fast." "It takes weeks." "The output is a 40-page document nobody reads."
Those objections are valid — against bad threat modeling. Good threat modeling is fast, lightweight, and produces a short list of concrete engineering tasks. This post shows you what that looks like in practice.
I'll walk through a threat model I built for a SaaS web application — a multi-tenant dashboard with user authentication, a REST API, and a PostgreSQL backend. Names and specifics are anonymized, but the methodology is exactly what I use.
What STRIDE Actually Is
STRIDE is a threat categorization framework developed at Microsoft. Each letter maps to a threat category:
| Letter | Threat | Violated Property |
|---|---|---|
| S | Spoofing | Authentication |
| T | Tampering | Integrity |
| R | Repudiation | Non-repudiation |
| I | Information Disclosure | Confidentiality |
| D | Denial of Service | Availability |
| E | Elevation of Privilege | Authorization |
STRIDE doesn't tell you how to attack a system. It gives you a checklist of categories to think through for each component and data flow in your system. The value is completeness — it's hard to forget a whole class of threats when you're walking through a structured framework.
Step 1: Draw the Data Flow Diagram
Before you can model threats, you need a map of your system. A DFD (Data Flow Diagram) shows:
- Processes — components that transform data (API servers, background jobs)
- Data stores — where data rests (databases, caches, file systems)
- External entities — actors outside your system boundary (browsers, third-party APIs, users)
- Data flows — how data moves between components
- Trust boundaries — lines where privilege or trust level changes
For our application, the Level 1 DFD looked like this:
[Browser] --HTTPS--> [Load Balancer] --HTTP--> [API Server]
|
[PostgreSQL]
|
[Background Worker]
|
[External Email API]
Trust boundaries:
- Between browser and load balancer (internet → DMZ)
- Between load balancer and API server (DMZ → internal network)
- Between API server and database (application → data tier)
- Between background worker and external email API (internal → internet)
Every data flow that crosses a trust boundary is a threat modeling target.
Step 2: Walk Each Component Through STRIDE
I work through STRIDE systematically for each process and data store. Here's the analysis for the API Server:
Spoofing
- Can an attacker impersonate a legitimate user? → Check JWT validation: is the signature verified? Is
alg: nonerejected? Are expired tokens rejected? - Can an attacker impersonate a legitimate service? → Check mTLS or API key validation on internal service calls
Finding: JWT library was configured with algorithms=["HS256", "RS256"] — an attacker who could control the alg header could potentially downgrade. Fixed by pinning to a single expected algorithm.
Tampering
- Can a user modify data they don't own? → Check authorization on every write endpoint — not just authentication
- Can data be modified in transit? → Verify HTTPS enforcement, HSTS headers
Finding: One bulk-update endpoint checked user.is_authenticated but not user.owns(resource). Classic IDOR. Added object-level authorization check.
Repudiation
- Can users deny having performed actions? → Check audit logging coverage
- Are logs tamper-resistant?
Finding: Admin actions were logged but regular user mutations were not. Added structured audit log for all state-changing operations.
Information Disclosure
- What data is returned in API responses? → Review response serializers for over-exposure
- Are stack traces returned on errors? → Check error handling in production config
- Are secrets in environment variables or hardcoded?
Finding: User profile endpoint returned password_hash and mfa_secret fields in the serialized response. Removed from serializer whitelist.
Denial of Service
- Are expensive endpoints rate-limited? → Check auth endpoints, search, file upload
- Can an attacker cause resource exhaustion?
Finding: Password reset endpoint had no rate limiting. Added per-IP and per-email rate limiting with exponential backoff.
Elevation of Privilege
- Can a regular user access admin functionality? → Test all admin endpoints with a non-admin token
- Can a tenant access another tenant's data?
Finding: Multi-tenancy filter was applied in middleware but one legacy endpoint bypassed middleware by loading data directly in the view. Refactored to enforce tenant scoping at the ORM layer.
Step 3: Score and Prioritize Findings
I use DREAD-lite scoring for prioritization — three dimensions, 1-3 scale each:
- Impact (1=low, 3=critical data/system compromise)
- Likelihood (1=requires insider access, 3=exploitable remotely without auth)
- Effort to fix (1=major refactor, 3=one-line change)
| Finding | Impact | Likelihood | Effort | Score |
|---|---|---|---|---|
| IDOR on bulk update | 3 | 3 | 2 | 8 |
| JWT alg confusion | 3 | 2 | 3 | 8 |
| Password reset rate limit | 2 | 3 | 3 | 8 |
| User data over-exposure | 2 | 2 | 3 | 7 |
| Missing audit logs | 1 | 3 | 2 | 6 |
The top three all went into the current sprint as P0 bugs. The others went into the security backlog.
Step 4: Output Is a Ticket List, Not a Document
The biggest mistake I made early in my threat modeling practice was producing a PDF report. Nobody reads it after the meeting. Instead, every finding from a threat model goes directly into the engineering backlog as a properly formatted bug ticket with:
- Description of the threat
- Reproduction steps or proof of concept
- Suggested fix
- STRIDE category and severity
The threat model document itself becomes a living diagram in your internal wiki — updated as the architecture changes, not a snapshot filed and forgotten.
How Long Does This Take?
For a system of this size (one API, one database, one async worker), the full exercise took:
- DFD + trust boundary mapping: 2 hours
- STRIDE walkthrough: 3 hours
- Scoring and ticket creation: 1 hour
Six hours of focused work, producing 5 actionable engineering tasks. That's a good return on investment for a feature that handles sensitive user data.
For a simple CRUD feature without multi-tenancy complexity, 2 hours total is realistic.
Tools I Use
- draw.io / Lucidchart — DFD diagrams
- OWASP Threat Dragon — browser-based tool with STRIDE built in, good for team workshops
- Microsoft Threat Modeling Tool — more heavyweight, better for complex enterprise systems
- Plain Markdown — honestly, for most features a structured Markdown table works fine
The tool matters less than the habit. A threat model done in a Google Doc is infinitely better than no threat model.