Threat Modeling with STRIDE for Application Design

Threat Modeling with STRIDE for Application Design - article cover image Security
Time it takes to read this article 8 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Threat modeling is the practice of systematically reasoning about how a system can be attacked before — or alongside — it is built, so that security requirements show up as design decisions instead of post-hoc patches. STRIDE is the most widely used taxonomy for structuring that reasoning: it decomposes “how could this be attacked” into six concrete threat categories — Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, and Elevation of privilege — mapped onto the security properties they violate. Originally developed at Microsoft in the late 1990s, STRIDE remains the default starting taxonomy in most secure-SDLC programs because it’s simple enough to teach non-security engineers and comprehensive enough to catch the threat classes that matter most in practice.

The mechanism that makes STRIDE tractable on a real system is applying it per-element of a data-flow diagram (DFD) rather than to the system as an undifferentiated whole. Each process, data store, external entity, and — critically — each trust boundary the data crosses gets evaluated against the six categories, turning an open-ended question into a checklist a design-review meeting can work through in an hour. This article walks that process against a concrete example application and produces the artifact a real session should leave behind: a per-category threat table with mitigations tied to each finding.

Attack Prerequisites

Threat modeling doesn’t have “attack prerequisites” in the exploitation sense — it’s an analysis technique, not a technique run against a live target. What has to be true for a STRIDE session to produce useful output:

  • A concrete architecture to model — at minimum a component diagram showing processes, data stores, external entities, and data flows; an under-specified design produces vague, unactionable threats.
  • Identified trust boundaries — every place data crosses from one privilege/trust level to another must be marked explicitly, because most real threats cluster at boundaries, not inside a single trusted component.
  • The right participants in the room — the engineer who built or will build the component, not just a security reviewer working from documentation alone.
  • A place to track findings to closure — a threat model nobody revisits is theater; findings need to become backlog items with owners.

How It Works

The standard workflow is: (1) decompose the system into a DFD — external entities, processes, data stores, data flows, and trust boundaries; (2) walk every element against all six STRIDE categories, asking “can this happen here”; (3) rate and record each credible threat; (4) assign a mitigation and an owner; (5) revisit the model whenever the architecture changes materially. The six categories map to the security property they threaten: Spoofing (authentication), Tampering (integrity), Repudiation (non-repudiation — can an actor deny having performed an action, due to weak logging/attribution), Information disclosure (confidentiality), Denial of service (availability), and Elevation of privilege (authorization — can an actor gain capabilities beyond what was granted).

Trust boundaries are where this pays off, because the same data flow has different threats on each side. A request crossing from an untrusted browser into a backend API is a Spoofing/Tampering question; the response crossing back out is an Information Disclosure question. Tools like OWASP Threat Dragon (a free DFD editor with built-in STRIDE prompts) and pytm (define the architecture as code and auto-generate a threat list and DFD from it) exist to formalize this instead of leaving it an unstructured whiteboard exercise nobody can diff against the next design revision.

For the worked example, consider a small expense-reporting web application: an employee (external entity, browser) submits a claim through a Web API (process), which validates it and writes it to an Expense DB (data store), then calls an external payment provider (external entity) via a signed API request. The DFD has three trust boundaries: (1) browser to Web API — the internet/corp-net boundary; (2) Web API to Expense DB — app-tier to data-tier, worth separating since a compromised API host and a compromised DB are different blast radii; (3) Web API to the external payment provider — a boundary crossing organizational control entirely.

Practical Example / Configuration

Walking the expense-reporting DFD element by element against STRIDE produces the kind of table a real design review should leave behind. This is the worked artifact for the boundary between the employee’s browser and the Web API, and between the Web API and the external payment provider:

Component: Employee Browser -> Web API  (trust boundary: internet/corp-net -> app)
-----------------------------------------------------------------------------
S - Spoofing:
  Threat: attacker submits claims as another employee via a stolen/guessed
  session token, or replays a captured auth cookie.
  Mitigation: strong session auth (short-lived JWT), MFA on login,
  SameSite=Strict + Secure + HttpOnly cookies.

T - Tampering:
  Threat: claim amount modified in transit, or a client-side parameter (e.g.
  'approved_amount') trusted from the request body.
  Mitigation: TLS 1.2+ end-to-end; server recomputes/validates all monetary
  fields, never trusts client-supplied totals or approval flags.

R - Repudiation:
  Threat: employee submits a fraudulent claim and later denies it; no reliable
  record ties the action to an authenticated identity and time.
  Mitigation: append-only audit log recording user ID, timestamp, source IP,
  and a hash of the submitted payload for every state-changing request.

I - Information Disclosure:
  Threat: GET /expenses/{id} does not check ownership, leaking other employees'
  expense data (IDOR).
  Mitigation: server-side authorization check on every read (owner_id ==
  session.user_id, or caller holds an approver role), not just on writes.

D - Denial of Service:
  Threat: bulk submission floods the claim endpoint or attaches oversized
  receipt images, exhausting storage/compute.
  Mitigation: per-user rate limiting, request size limits, async queue for
  receipt OCR so a burst doesn't block the request path.

E - Elevation of Privilege:
  Threat: a standard employee calls the manager-only PATCH
  /expenses/{id}/approve endpoint directly, bypassing the UI's role check.
  Mitigation: enforce role checks server-side on every endpoint, independent
  of the UI; deny-by-default authorization middleware.

Component: Web API -> External Payment Provider  (trust boundary: org -> 3rd party)
-----------------------------------------------------------------------------
S - Spoofing: rogue service/MITM impersonates the provider's callback endpoint
  to fake a 'payment sent' confirmation. Mitigation: mutual TLS or signed-
  webhook (HMAC) verification on all inbound callbacks.
T - Tampering: reimbursement amount altered in the outbound request.
  Mitigation: request signing (HMAC/JWS) over the full payload.
I - Information Disclosure: employee bank/routing details logged in plaintext.
  Mitigation: field-level redaction in logging middleware for PII/financial
  fields.
TEXT

A pytm definition of the same boundary formalizes the model as code so it can be version-controlled and diffed on every architecture change:

from pytm import TM, Server, Datastore, Dataflow, Boundary, Actor

tm = TM("Expense Reporting App")
internet, app_tier = Boundary("Internet"), Boundary("App Tier")
employee = Actor("Employee Browser"); employee.inBoundary = internet

web_api = Server("Expense Web API")
web_api.inBoundary = app_tier
web_api.protocol, web_api.authenticatesSource = "HTTPS", True

expense_db = Datastore("Expense DB")
expense_db.inBoundary, expense_db.isEncryptedAtRest = app_tier, True

Dataflow(employee, web_api, "Submit Claim").protocol = "HTTPS"
Dataflow(web_api, expense_db, "Persist Claim").protocol = "TLS"

tm.process()   # generates DFD + a STRIDE-categorized threat list for review
Python

Walkthrough / Exploitation

A design-review session runs the same procedure every time, which is what makes it repeatable across teams:

1. Draw or update the DFD: external entities, processes, data stores, data flows.
2. Mark every trust boundary the data crosses explicitly on the diagram.
3. For each element touching a boundary, walk all six STRIDE letters and ask
   'can this happen here, given how this specific component is built.'
4. Record every credible threat with a mitigation and an owner -- discard
   non-credible ones explicitly (one-line reason) rather than silently dropping.
5. File each unmitigated threat as a tracked backlog item, not a document footnote.
6. Re-run the model when the architecture changes -- a new integration or trust
   boundary is the trigger, not a calendar date.
TEXT

Note: STRIDE catches design-level threats, not implementation bugs — it correctly flags “the API must authorize every read” as a requirement, but won’t catch a specific IDOR that slips through implementation. Pair it with code review, SAST, and pentesting; it’s a complementary layer, not a replacement.

Opsec: Store completed threat models alongside the architecture docs they describe, in version control, so they’re diffable against future changes — a model that lives only in a slide deck goes stale within a quarter.

Detection and Defense

STRIDE itself is a design-time technique, but the artifacts it produces should feed directly into detection and operational controls:

  • Integrate into design reviews as a gate — no new external-facing data flow or trust-boundary crossing ships without a STRIDE pass, ideally using pytm or Threat Dragon so the model is a versioned artifact, not a one-off meeting.
  • Track mitigations to closure — link each threat-table row to a ticket; an unmitigated finding sitting in a document nobody reopens is equivalent to not having found it.
  • Turn Repudiation mitigations into real detections — the audit-log requirement should map to an actual SIEM alert, e.g. an approval action from an account that never authenticated via MFA.
  • Revisit on architecture change — a new integration, auth method, or microservice crossing an existing boundary should trigger a delta review.
  • Cross-reference against a control framework — mapping findings to OWASP ASVS or a NIST 800-53 control family gives auditors a traceable link from threat to control.

A mature program is measured by the closure rate of findings, not the count of models produced — a recurring finding class (e.g. missing server-side authorization) is a signal to fix the pattern architecturally rather than endpoint by endpoint.

Real-World Impact

STRIDE has been Microsoft’s baseline threat-modeling methodology since the SDL (Security Development Lifecycle) was formalized in the mid-2000s and remains the default taxonomy in OWASP’s Threat Modeling guidance and tooling (Threat Dragon, pytm). Organizations that adopt structured threat modeling as a mandatory design-review gate — rather than an ad hoc activity — consistently report catching authorization and data-exposure flaws earlier and cheaper than the same flaws found via pentest or, worse, in production.

Conclusion

STRIDE turns “how could this be attacked” from an open-ended, personality-dependent exercise into a repeatable checklist applied per component and per trust boundary on a data-flow diagram. The value is entirely in the artifact it leaves behind: a threat table with real mitigations and owners, versioned alongside the architecture, revisited whenever that architecture changes, and tracked to closure like any other engineering backlog item.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL