Skip to main content
Security for Application Engineers

Threat Modeling per Feature

Ravinder··7 min read
SecurityAppSecThreat ModelingSTRIDE
Share:
Threat Modeling per Feature

Most security problems are design problems wearing runtime clothes. By the time a SQL injection or a broken access control surfaces in production, the architecture that made it possible was committed weeks or months ago. Threat modeling is how you move that conversation earlier — not into a security team's quarterly review, but into the sprint ceremony where the feature is first sketched out.

The catch is that classical threat modeling is heavy. STRIDE workshops with dedicated security architects, data-flow diagrams in Microsoft Threat Modeling Tool, multi-day facilitated sessions — these belong to the pre-cloud era of annual releases. If your team ships every two weeks, the model has to fit inside a refinement meeting.

This post shows a lightweight, per-feature STRIDE approach that a team of four can run in under an hour.

Why per Feature, Not per Quarter

A quarterly threat model reviews a snapshot of a system that has already changed. The design decisions that introduced risk were made without that input. Per-feature threat modeling puts security thinking at the same moment as the architecture sketch — when changing course is cheap.

The goal is not a perfect threat document. The goal is: does this feature have an obvious, exploitable flaw we haven't thought about? If the answer is yes, you capture it as an acceptance criterion or a separate security task before estimation.

STRIDE in One Paragraph

STRIDE is a mnemonic for six threat categories:

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

You walk each component in your feature's data-flow through each row and ask: can an attacker do this here?

The Lightweight Workflow

Three artifacts, thirty to fifty minutes:

  1. A component sketch — a whiteboard or Excalidraw diagram showing actors, services, data stores, and the trust boundaries between them.
  2. A STRIDE table — one row per component, six columns.
  3. Mitigations as acceptance criteria — any "yes" cell becomes a ticket or an AC on the epic.

That is it. No tooling requirement, no dedicated security team needed on day one.

Example: A File-Upload Feature

Suppose you are building a user file-upload feature: a browser uploads a file to an S3-backed API, which stores metadata in a relational database and triggers an async virus scan.

Actor: Browser (untrusted)
  → API Gateway (trust boundary)
    → Upload Service
      → S3 Bucket
      → Metadata DB
      → Scan Queue → Virus Scanner → Scan Results DB

Running STRIDE against the Upload Service component:

STRIDE Question Finding
Spoofing Can an attacker upload as another user? Missing user-id binding in upload token
Tampering Can a file be swapped between upload and scan? S3 object should be immutable until scan completes
Repudiation Can a user deny uploading a file? Need audit log: who uploaded, when, from where
Information Disclosure Can one user download another's file? Pre-signed URL scope must be per-user
Denial of Service Can an attacker exhaust storage? Need per-user quota enforcement
Elevation of Privilege Can the scanner be tricked into executing code? Scanner runs in isolated sandbox, no shell access

Three of those six cells surface real bugs that would otherwise live in production.

Mermaid: Feature Data-Flow with Trust Boundaries

flowchart LR subgraph Untrusted["Untrusted Zone"] Browser["Browser / Client"] end subgraph DMZ["API Layer (TLS boundary)"] GW["API Gateway"] US["Upload Service"] end subgraph Backend["Internal Zone"] S3["S3 Bucket"] DB["Metadata DB"] Q["Scan Queue"] VS["Virus Scanner\n(isolated)"] end Browser -- "HTTPS POST /upload\n+ JWT" --> GW GW -- "validates JWT,\nforwards request" --> US US -- "PutObject (IAM role)" --> S3 US -- "INSERT metadata" --> DB US -- "enqueue job" --> Q Q --> VS VS -- "UPDATE scan_status" --> DB

The trust boundaries — Untrusted → DMZ and DMZ → Backend — are where most STRIDE findings cluster. Make them explicit.

Embedding This in Your Sprint Process

The practical cadence that works in high-velocity teams:

Refinement agenda (55 min total):
  - Feature walkthrough & acceptance criteria  [30 min]
  - Draw component sketch on whiteboard        [ 5 min]
  - Walk STRIDE table together                 [15 min]
  - Capture findings as ACs or tasks          [ 5 min]

The component sketch does not need to be formal. A box-and-arrow picture on a whiteboard, photographed into Confluence, is enough. The STRIDE table can live in the ticket description.

Threat Libraries Speed Things Up

After a few months you will notice the same threats recurring across features: broken access control on user-scoped resources, missing audit logs, file-type bypass on uploads. Capture these as a team threat library — a short Markdown file in your repo listing common patterns and their standard mitigations. New engineers can run through the library in thirty minutes and immediately contribute to threat reviews.

## Threat Library Entry: User-Scoped Resources
 
**Threat (STRIDE: I + E):** API endpoint returns records without
  verifying the authenticated user owns the record.
 
**Standard mitigation:**
  - All queries must include `WHERE user_id = :current_user_id`
  - Integration test: authenticated user A cannot fetch user B's resource
  - Code review checklist item: "Is ownership enforced in the query?"

Automating the Skeleton

You can generate the STRIDE table skeleton from an OpenAPI spec or a service's README. A simple script reads the paths and produces a prefilled Markdown table — reviewers fill in the findings column, not the boilerplate.

import yaml, sys
 
STRIDE = ["Spoofing", "Tampering", "Repudiation",
          "Info Disclosure", "Denial of Service", "Elevation"]
 
def generate_stride_table(spec_path: str) -> str:
    with open(spec_path) as f:
        spec = yaml.safe_load(f)
 
    rows = ["| Component | " + " | ".join(STRIDE) + " |",
            "|-----------|" + "---|" * len(STRIDE)]
 
    for path, methods in spec.get("paths", {}).items():
        for method in methods:
            component = f"`{method.upper()} {path}`"
            cols = " | ".join(["?" for _ in STRIDE])
            rows.append(f"| {component} | {cols} |")
 
    return "\n".join(rows)
 
if __name__ == "__main__":
    print(generate_stride_table(sys.argv[1]))

Run this at the start of a refinement meeting and you have a ready-made table to annotate.

When to Escalate

Lightweight threat modeling catches the obvious. Some features warrant a deeper review:

  • Cryptographic key management changes
  • Authentication or authorization redesigns
  • Cross-tenant data access
  • External partner integrations

For these, escalate to a security engineer or schedule a dedicated threat modeling session. The lightweight session still runs first — it surfaces questions faster and makes the deep review more productive.

Key Takeaways

  • Threat modeling belongs in refinement, not in a separate security ceremony — the cheapest time to fix a design flaw is before the sprint starts.
  • STRIDE gives you a six-category checklist that non-security engineers can apply without deep expertise.
  • Draw the trust boundaries explicitly; most findings cluster at those edges.
  • Every "yes" cell in the STRIDE table becomes an acceptance criterion or a dedicated task, not a verbal agreement.
  • A team threat library of recurring patterns dramatically accelerates future sessions and onboards new engineers quickly.
  • Escalate to deeper review for cryptography, AuthN/AuthZ redesigns, and cross-tenant features — lightweight is not a substitute for all security review.
Share: