| name | threat-model-stride |
| description | Produces a design-level STRIDE threat model — decomposes the architecture into a data-flow diagram with trust boundaries, enumerates threats per element, rates them by likelihood × impact, and records mitigations and signed-off residual risk. Use before building or substantially changing a system that handles untrusted input, secrets, money, or PII. |
| when_to_use | A new service, public API, auth flow, multi-tenant boundary, or agent/tool surface is being designed and you need "what could go wrong here?" answered before code exists. Distinct from security-review (audits an already-written diff line by line) and write-rfc (proposes the design itself). |
When to Use
Reach for this when the question is what an adversary could do to a design, before the design is built:
- "Threat model this new payments/checkout service"
- "We're adding a multi-tenant boundary — where can one tenant reach another's data?"
- "New public API / webhook ingress / agent tool surface — enumerate the attack surface"
- "The security RFC needs a threats section and a residual-risk register"
- "What trust boundaries does this auth flow cross, and what crosses each one?"
NOT this skill:
- Auditing already-written code for injection/SSRF/secrets line by line → security-review (this skill works on a diagram, not a diff)
- Writing the design/proposal itself (motivation, alternatives, rollout) → write-rfc (threat-model is one section feeding it)
- Implementing the login/JWT/session controls a threat surfaces → auth-jwt-session
- Storing/rotating the secrets a threat targets → secrets-management
- Hardening one webhook endpoint's signature/replay handling → ingest-webhook-secure
- Responding to an attack happening now → incident-response-sre
Steps
-
Define scope, assets, and adversaries first — never enumerate threats against an unbounded system. Write three lists before drawing anything:
-
Assets — what you protect, by category: confidentiality (PII, secrets, tokens), integrity (balances, order state, audit log), availability (checkout, login). Name the concrete data, not "the database."
-
Adversaries — pick from this set and state each one's starting position:
| Adversary | Starts with | Typically drives |
|---|
| Anonymous internet | Network reachability only | S, D, I (info disclosure via errors) |
| Authenticated user | Valid session, own tenant | E (priv-esc), tenant-boundary I/T |
| Malicious tenant (multi-tenant) | Valid account, own data | Cross-tenant I (read), T (write) |
| Insider / operator | Prod console, some creds | R (repudiation), I, E |
| Compromised dependency | Code execution in one process | S, T, E across the process boundary |
| Stolen credential / token | One leaked secret | S, blast-radius of that secret |
-
In/out of scope — explicitly list what you will NOT model (e.g. "physical datacenter security: out; we trust the cloud provider's hypervisor"). Unstated scope = infinite scope.
-
Draw the data-flow diagram as validated Mermaid — four element types plus boundaries, no more. DFD elements: External Entity (square — user, third-party API), Process (round — your service/lambda), Data Store (cylinder — DB, queue, bucket, cache), Data Flow (arrow, labeled with protocol + what data). A trust boundary is a dashed box crossing one or more flows where the privilege/trust level changes. The four boundaries to always look for: network edge (internet → DMZ), authz (unauthenticated → authenticated), tenant (tenant A → shared/tenant B), process (your code → third-party/dependency code).
flowchart LR
subgraph edge["Network edge — untrusted"]
user["Browser (external entity)"]
end
subgraph trusted["Authenticated · single-tenant"]
api("API service")
worker("Async worker")
end
db[("Orders DB")]
pay["Stripe API (external entity)"]
user -->|"HTTPS · login creds"| api
api -->|"SQL · tenant_id scoped"| db
api -->|"enqueue · job payload"| worker
worker -->|"HTTPS · card token"| pay
Common Errors
- Listing threats with no diagram. Without the DFD you miss the boundary-crossing flows that produce the real threats. Draw and validate the diagram first; enumerate per element second.
- Missing trust boundaries entirely (or only drawing the network edge). The expensive bugs live at the authz and tenant boundaries, not the firewall. Every place trust level changes gets a dashed box.
- Vague threats: "data could be leaked." Unactionable and unrateable. Write the concrete path: which actor, which element, which parameter, which STRIDE letter.
- Skipping STRIDE letters because they "feel unlikely." That's what rating is for — enumerate all applicable letters per element, then let likelihood × impact triage. Skipping at enumeration time hides the threat; skipping at rating time is a defensible decision.
- Accepting risk with no owner/expiry. "We'll accept that" in a meeting evaporates. An accepted risk is only accepted when it's in the register with a name, a date, and a re-review trigger.
- Modeling the whole company. Scope creep makes the model useless. Bound it to the one service/flow/change and explicitly list what's out of scope (step 1).
- Rating on aspirational controls. Rating a threat "Low" because of a mitigation you plan to build inflates safety. Rate on what exists today; the disposition step is where planned controls earn their reduction.
- Mitigation = "add validation" / "we'll be careful." Not a control. Name the mechanism (RLS policy, signed token with
aud check, allowlist, rate limiter) and the task that builds it.
- Treating it as one-and-done. A model with no re-model triggers is stale the next time a boundary moves. List the triggers that force a redo.
- Confusing this with a code audit. STRIDE on a diagram finds design flaws (missing boundary, IDOR by design); it will not find a SQL-injection typo in line 88 — that's security-review on the diff.
Verify
- Diagram renders:
npx -y @mermaid-js/mermaid-cli -i model.mmd -o model.svg exits 0 and the SVG shows every external entity, process, store, flow, and at least one dashed trust boundary.
- Boundary coverage: Every flow that crosses a trust boundary has at least one threat row. Pick any boundary-crossing arrow at random — it must appear in the threat table.
- STRIDE coverage: Each process element was evaluated against all six letters (each letter either has a threat row or an explicit "N/A — why"); stores and flows covered for T/I/D.
- Every threat is concrete and rated: No row reads "data could leak"; each names actor + element + attack, carries a likelihood × impact rating, and is sorted Critical-first.
- Every threat is dispositioned: Each row is exactly one of mitigate / eliminate / transfer / accept — zero "noted" or blank. Mitigations name a control and link a tracked task.
- Residual register complete: Every Accept appears in the residual-risk register with owner, sign-off, and expiry. No Critical/High is in Accept without explicit named sign-off.
- Abuse cases + re-model triggers present: The doc lists the top attacker stories and the events that invalidate the model.
Done = the Mermaid DFD renders with explicit trust boundaries, every boundary-crossing flow has STRIDE-enumerated threats that are each rated and dispositioned, no Critical/High sits in Accept without named sign-off, and the doc ships a residual-risk register plus re-model triggers.