threat-modeling

Every methodology — STRIDE, PASTA, LINDDUN, attack trees — is a technique for

SKILL.md

GitHub

Threat Modeling (Verbose)

Core Patterns

The Four Questions

Every methodology — STRIDE, PASTA, LINDDUN, attack trees — is a technique for answering four questions in order:

1. What are we building?      Data flow diagram with trust boundaries
2. What can go wrong?         STRIDE, per element
3. What will we do about it?  Eliminate / mitigate / transfer / accept
4. Did we do a good job?      Tracked issues, tests, and a re-review trigger

Teams that skip question 1 produce generic threat lists. Teams that skip question 3 produce documents. Teams that skip question 4 produce documents that are wrong within a quarter.

Data Flow Diagrams and Trust Boundaries

An architecture diagram shows what talks to what. A DFD shows where trust changes, which is where threats live. Four notations:

Shape Meaning Example
Rectangle External entity — outside your control Browser, OCR vendor
Circle Process — code you run API service, PDF renderer
Parallel lines Data store Postgres, S3, Redis
Arrow Data flow HTTPS request, SQL query
Dashed line Trust boundary Internet edge, tenant edge, org edge

Worked example — an expense reimbursement SaaS:

                        ===== BOUNDARY A: internet / DMZ =====
 [Browser (SPA)] --1. POST /api/v1/expenses {amount, receipt} (JWT)--> ( API service )
 [Finance admin] --2. POST /api/v1/expenses/:id/approve (JWT)-------->

                        ===== BOUNDARY B: app / data =====
 ( API service ) --3. SQL over TLS, app role----------------> || Postgres ||
 ( API service ) --4. PutObject------------------------------> || S3 receipts ||
 ( API service ) --5. audit event----------------------------> || Audit log (append-only) ||

                        ===== BOUNDARY C: tenant isolation (logical) =====
 all queries carry tenant_id — enforced where?

                        ===== BOUNDARY D: organization / vendor =====
 ( OCR worker ) --6. HTTPS receipt image + API key----------> [ OCR vendor ]
 [ Stripe ] -----7. POST /webhooks/stripe-------------------> ( API service )

                        ===== BOUNDARY E: build / supply chain =====
 [ npm, PyPI, base images ] --8. build----------------------> ( CI ) --9. deploy--> ( API service )

Reading the boundaries produces findings before STRIDE even starts:

  • Boundary C is logical, not physical. It exists only if every query filters on tenant_id. That is an assumption, and it needs an enforcement point — row-level security in Postgres, or a repository layer nothing can bypass. "Developers will remember" is not a boundary.
  • Flow 6 crosses out of the organization carrying PII. That is a data processing agreement, a retention question, and a vendor-breach risk, none of which is visible on an architecture diagram.
  • Flow 7 arrives unauthenticated from the internet. Webhook signature verification is the only thing making the sender trustworthy.
  • Boundary E exists at all. CI holds deploy credentials and executes third-party code. Most diagrams omit it entirely.

STRIDE, Per Element

STRIDE maps threat categories to the security property they violate:

Category Violates Question to ask
Spoofing Authentication Can someone claim to be another principal?
Tampering Integrity Can data be modified in flight or at rest?
Repudiation Non-repudiation Can an actor deny what they did?
Information disclosure Confidentiality Can data reach someone unauthorized?
Denial of service Availability Can service be exhausted or degraded?
Elevation of privilege Authorization Can someone gain rights they were not granted?

Not every category applies to every element. Use the standard mapping:

Element S T R I D E
External entity ✓ ✓
Process ✓ ✓ ✓ ✓ ✓ ✓
Data store ✓ ✓ ✓ ✓
Data flow ✓ ✓ ✓

A full pass over the API service (process — all six apply):

# Cat Threat Disposition Control
T-01 S JWT accepted with alg: none or RS256→HS256 confusion Mitigate Pin algorithms=["RS256"]; verify aud and iss
T-02 S Stripe webhook forged by anyone who knows the URL Mitigate Verify the signature header against the signing secret; reject on clock skew > 5 min
T-03 T Client submits approved: true in the create payload Mitigate Whitelist writable fields; approval state set only by the approval handler
T-04 T Amount edited after approval, record still reads approved Mitigate Approval stores a hash of the approved row; re-approval required on change
T-05 R Approver denies approving a $40k expense Mitigate Append-only audit log with actor, timestamp, and request id; shipped to an account the app cannot write to
T-06 I GET /expenses/:id returns another tenant's record (IDOR) Mitigate Postgres RLS on tenant_id, plus a repository layer that refuses an unscoped query
T-07 I Stack traces in 500 responses leak schema and library versions Mitigate Generic error body; details to logs only
T-08 D 10,000 PDF export jobs queued by one tenant Mitigate Per-tenant token bucket; bounded worker pool; queue depth alert
T-09 E SSRF in the PDF renderer reaches the cloud metadata endpoint Mitigate Egress allowlist from the renderer; IMDSv2 required; renderer runs with no cloud role
T-10 E API pod role can read the payment provider secret it never uses Eliminate Remove the grant; only the billing worker holds it

And over the non-process elements, where the categories differ:

# Element Cat Threat Disposition
T-11 ` Postgres
T-12 ` Postgres
T-13 ` Audit log
T-14 Flow 6 (to vendor) I Receipt PII leaves the org and is retained indefinitely Transfer + accept — DPA signed, 30-day retention contractually required, reviewed each renewal
T-15 Flow 3 (app→DB) I TLS not enforced; a compromised node can read queries Mitigate — sslmode=verify-full
T-16 [OCR vendor] S Vendor response spoofed by anyone able to intercept Mitigate — certificate validation on; treat OCR output as untrusted input

Sixteen concrete, assigned threats from one hour on a whiteboard.

Prioritize Without Pretending to Be Precise

DREAD's numeric scores imply an accuracy nobody has. A likelihood × impact grid, argued out loud, is more honest and takes less time.

Impact: Low Impact: Medium Impact: High
Likely T-07 T-08 T-06, T-01
Possible T-16 T-15, T-11 T-09, T-03
Unlikely — T-12 T-05, T-14

Fix the top-right first. What matters is not the cell but the disagreement that surfaces while placing each item — that argument is where the real assumptions get exposed.

Design Time Beats Ship Time

Threat model (design) Pentest (ship)
Finds Classes of flaw, missing controls Instances, exploitable today
Sees Intent, trust boundaries, assumptions Only the deployed surface
Cost to fix a finding Whiteboard edit Rework, migration, sometimes redesign
Blind to Implementation bugs, config drift Absent controls nobody built
Cadence Every significant design change Periodic, point-in-time

A pentest reports "IDOR on GET /expenses/:id". A threat model reports "no enforcement point for tenant isolation exists", which explains that endpoint and predicts the other forty-two. The pentest finds the instance; the model finds the class — and finds it in week one rather than month nine, when tenant scoping is still one repository layer instead of a data migration and an audit of every query ever written.

These are complements. Model at design to decide what to build; test at ship to verify you built it. Confirming which modeled threats are actually live in the running system is the vulnerability-assessment skill's job.

Close the Loop

T-06  IDOR on expense reads
      -> PRO-214  add RLS policy + repository guard
      -> test: tests/security/test_tenant_isolation.py::test_cross_tenant_read_404
      -> re-review trigger: any new table holding tenant-scoped data

Every mitigated threat should end at a test, so a regression fails the build rather than waiting for the next review.

Common Anti-Patterns

❌ Architecture diagram with no trust boundaries — you enumerate components instead of transitions, and the threats live at the transitions. ✅ Draw boundaries first; every crossing gets scrutiny.

❌ Omitting CI, vendors, and webhooks from the diagram — the unmodeled element is the unmitigated one. ✅ If it can send you bytes or hold your credentials, it is on the diagram.

❌ Applying all six STRIDE categories to every element — you invent "spoofing the database" and lose an hour. ✅ Use the element-to-category mapping.

❌ A logical trust boundary with no enforcement point — "queries include tenant_id" is a convention, and conventions are one pull request from being false. ✅ Enforce in one place code cannot bypass: RLS, a repository layer, a proxy.

❌ Threats with no disposition — a list of everything that could go wrong, owned by nobody. ✅ Every threat gets eliminate / mitigate / transfer / accept, plus an owner.

❌ Accepting risk anonymously — "we accepted that" with no name or date is indistinguishable from having missed it. ✅ Record who accepted, why, and when it is revisited.

❌ DREAD scores to one decimal place — false precision on guessed inputs. ✅ A likelihood × impact grid, argued in the room.

❌ Modeling once, at the start — the design changed the week after. ✅ Trigger a re-review on new trust boundary, new data class, or new integration.

❌ "Input validation" as the answer to everything — it addresses tampering and some injection, and does nothing for authorization, repudiation, or DoS. ✅ Match the control to the STRIDE category it actually covers.

Threat Modeling Checklist

  • DFD drawn with external entities, processes, data stores, and flows
  • Trust boundaries explicitly marked, including tenant and organization edges
  • CI/CD, third-party vendors, and webhook senders present on the diagram
  • Each logical boundary has a named enforcement point in code or infra
  • STRIDE applied per element using the element-to-category mapping
  • Data stores reviewed for tampering, disclosure, and repudiation
  • Data flows crossing boundaries reviewed for interception and tampering
  • Audit logging reviewed for integrity, not just presence
  • Every threat has a disposition and a named owner
  • Accepted risks record who accepted, why, and the review date
  • Prioritized by likelihood × impact, not by numeric pseudo-precision
  • Mitigations linked to tickets, and tickets linked to tests
  • Re-review triggers defined (new boundary, new data class, new integration)
  • Model performed at design time, with a pentest scheduled to verify