Skip to content
← Back to Blog

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.

·6 min read··
Share

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:

LetterThreatViolated Property
SSpoofingAuthentication
TTamperingIntegrity
RRepudiationNon-repudiation
IInformation DisclosureConfidentiality
DDenial of ServiceAvailability
EElevation of PrivilegeAuthorization

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:

  1. Between browser and load balancer (internet → DMZ)
  2. Between load balancer and API server (DMZ → internal network)
  3. Between API server and database (application → data tier)
  4. 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: none rejected? 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)
FindingImpactLikelihoodEffortScore
IDOR on bulk update3328
JWT alg confusion3238
Password reset rate limit2338
User data over-exposure2237
Missing audit logs1326

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.

Discussion

© 2026 Anilkumar · Product Security Engineer