Build a Simple Threat Model

Learn to build a simple threat model for an app. Identify assets, trust boundaries, and attack paths in a step-by-step walkthrough, then test your skills with a practical exercise. Ideal for developers who want to think like an attacker and harden their apps.

Focus: build a simple threat model for an app

Sponsored

You’ve built the app. You’ve shipped it. But did you build it to withstand an attack? Most developers only discover their security blind spots after a breach — when the damage is already done. The fix is to build a simple threat model for an app before you write another line of code. A threat model is your security blueprint: it forces you to think like an attacker, identify what’s worth stealing, and map out every possible way in. In this Security foundations lesson, you’ll learn a practical, no-fluff method to create one — no PhD in cryptography required.

The problem this lesson solves

Every app has assets: user credentials, payment data, API keys, even the integrity of your business logic. Attackers know this. They don’t hack “the system” — they exploit a single weakness in a single component that leads to something valuable. Without a threat model, you’re defending blind.

Consider a typical web app: a React frontend, a Python Flask backend, a PostgreSQL database, and a third-party payment API. Where are the risks? If you say “the database,” you’re only seeing one dot on the map. The real vulnerabilities live in the connections between components — the Authorization header that isn’t validated on every endpoint, the database that’s reachable from the public internet, the log file that stores raw card numbers. A threat model surfaces these edge cases before they become incidents.

The cost of ignoring this is real: data breaches cost millions, erode customer trust, and can put you out of business. But the good news? A simple threat model takes a few hours, not weeks. This lesson shows you exactly how.

Core concept / mental model

Think of threat modeling like mapping a bank vault:

  • Assets are the money — the data and functionality that matters.
  • Trust boundaries are the walls and locked doors — where privilege or data flows change.
  • Attackers are the thieves — motivated insiders or outsiders.
  • Threats are the specific break-in techniques — picking the lock, bribing a guard, or crawling through the duct.

Your job is to walk through every room and ask: How would someone get in, and what would they take along the way?

A useful formalization is STRIDE, developed by Microsoft. It’s a mnemonic for six types of threats:

| Threat | What it violates | Example | | --- | --- | --- | | Spoofing | Authenticity | User logs in as another user | | Tampering | Integrity | Data altered in transit | | Repudiation | Non-repudiation | User denies an action they performed | | Information disclosure | Confidentiality | Leaked password hashes | | Denial of service | Availability | Overload the server | | Elevation of privilege | Authorization | Normal user gains admin rights |

You don’t need to enumerate every instance of STRIDE. You need to scan your architecture and ask which of these six does my app face in each trust boundary? This keeps your thinking structured instead of chaotic.

How it works step by step

The process is simple, repeatable, and works for any app — from a monolith to a serverless microservices stack. Here are the five steps:

  1. Decompose the app. Draw a diagram of every component, data flow, and user role. Include third-party services, databases, and internal modules.
  2. Identify assets. What does an attacker want? Rank them by value: credentials > payment data > business secrets > PII > session tokens.
  3. Draw trust boundaries. Mark where data crosses from a public zone to a private one, or where a lower-privileged user touches a higher-privileged component. Example: "User input → backend API" is a boundary.
  4. Apply STRIDE to each component and boundary. For each trust boundary, ask: Can an attacker spoof, tamper, repudiate, disclose, DoS, or elevate privileges here?
  5. Rank and mitigate. Score each threat by likelihood × impact (e.g., High/Medium/Low). Fix the high ones first.

It’s that simple — but the magic is in the rigor. You’re predicting the attack, not reacting to it.

Hands-on walkthrough

Let’s apply this to a real example: a Python Flask note-taking app with SQLite, user logins, and a REST API.

Step 1: Decompose the app

Here’s a text-based diagram:

[Browser] --HTTPS--> [Flask app] --SQL--> [SQLite DB]
                       |
                       |--API--> [Email verification service]

Step 2: Identify assets

  • User credentials (username, password hash)
  • User’s notes (private content)
  • Session tokens
  • App’s API keys (email service)

The most valuable asset: user credentials, because they can pivot to other services (password reuse).

Step 3: Draw trust boundaries

  • Boundary 1: Browser ↔ Flask app — public internet, unauthenticated initially.
  • Boundary 2: Flask app ↔ SQLite — trusted internal, but only if the app is well-written.
  • Boundary 3: Flask app ↔ Email service — egress to a third party.

Step 4: Apply STRIDE

Let’s take Boundary 1 (public internet). Which threats apply?

  • Spoofing: An attacker could guess a session token or brute-force a password.
  • Tampering: If HTTPS isn’t enforced, notes could be modified in transit.
  • Repudiation: Without logs, a user could claim they never posted a note.
  • Information disclosure: If error messages reveal whether a username exists, that’s a user-enumeration leak.
  • Denial of service: A flood of login attempts could lock out the server.
  • Elevation of privilege: If the session token contains the user ID unsafely, an attacker could change it to admin.

Next, Boundary 2 (Flask ↔ SQLite):

  • Tampering: SQL injection if queries aren’t parameterized.
  • Information disclosure: Database file world-readable on disk.

Boundary 3 (email service):

  • Information disclosure: Logging the API key in the app logs.
  • Tampering: If the email API key is exposed, an attacker could send phishing emails as you.

Step 5: Rank and mitigate

| Threat | Likelihood | Impact | Priority | Mitigation | | --- | --- | --- | --- | --- | | User enumeration | Medium | Low | Medium | Uniform error messages | | SQL injection | Low (if parameterized) | High | High | Use parameterized SQL | | Session hijack | Medium | High | High | Use HttpOnly, Secure cookies; rotate tokens | | API key leak | Low | High | High | Store in env vars, never log | | DoS on login | High | Medium | Medium | Rate limiting |

Now, let’s see a concrete vulnerability and its fix. Suppose your Flask app has this code:

from flask import Flask, request, session
import sqlite3

app = Flask(__name__)
app.secret_key = 'hardcoded-secret'

def get_user(username):
    conn = sqlite3.connect('notes.db')
    # DANGER: string formatting → SQL injection
    cur = conn.execute(f"SELECT * FROM users WHERE username = '{username}'")
    return cur.fetchone()

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    user = get_user(username)
    if user and user[2] == password:
        session['user'] = user[0]
        return 'Logged in'
    return 'Invalid credentials', 401

This code violates two STRIDE threats:

  • Tampering (SQL injection)
  • Spoofing ( hardcoded secret, plain text password)

The threat model identified these as high-priority. Here’s the fixed version:

import os
import sqlite3
from flask import Flask, request, session
from werkzeug.security import check_password_hash

app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY')  # Use env var

def get_user(username):
    conn = sqlite3.connect('notes.db')
    # SAFE: parameterized query prevents injection
    cur = conn.execute("SELECT * FROM users WHERE username = ?", (username,))
    return cur.fetchone()

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    user = get_user(username)
    if user and check_password_hash(user[2], password):
        session.clear()
        session['user'] = user[0]
        return 'Logged in'
    return 'Invalid credentials', 401

Now the threats are mitigated:

  • SQL injection is blocked by parameterization.
  • Password is hashed, so a leaked database doesn’t expose plaintext.
  • Secret key comes from the environment, not the code.

The threat model told you what to fix; the code shows how.

Compare options / when to choose what

You don’t always need a whiteboard session. Here’s how different threat-modeling styles compare:

Method Best for Effort Output
STRIDE General web apps, API design Medium Six threat categories
Attack trees Deep-dive on one attack path High Decision-tree of attacker actions
DREAD (rating) Prioritizing risks Low Numeric scores for each threat
Security stories Agile teams, rapid iteration Low User-story-style threat statements

For most developers, STRIDE + simple rating is the sweet spot: structured but fast. Attack trees are overkill unless you’re dealing with critical infrastructure. DREAD is a nice add-on to score your STRIDE findings — combine them for clarity.

Troubleshooting & edge cases

Threat modeling goes wrong when you overthink or underthink. Here are common pitfalls:

  • “I don’t need this for a small app.” Wrong — small apps get hacked all the time because there’s no process. The risk is proportional to data, not code size.
  • “I listed everything but did nothing.” A threat model isn’t a deliverable; it’s a conversation. You must act on the high-priority items.
  • “I only thought about external attackers.” Insider threats or supply-chain attacks (e.g., a compromised open-source dependency) are equally dangerous.
  • Misplaced trust boundaries: Many developers forget that their own backend is not automatically a trust boundary — if an attacker can reach the API directly, the boundary is at the frontdoor, not at the server.

A simple tool to avoid these mistakes is to ask “what does an attacker get for $10?” — if the attack costs more than the value, it’s not worth defending. That’s your risk formula.

What you learned & what's next

You now know the core concept of threat modeling: understand your assets, trust boundaries, and potential STRIDE attacks, then prioritize fixes. You’ve seen both a vulnerable and a hardened Flask app, and you know the difference between listing threats and mitigating them.

You met both learning objectives: you can explain the core idea and you completed a practical exercise. This skill is a prerequisite for everything else in security — from secure coding to penetration testing.

Your next step is to practice on your own app. Take one feature you built (a login, a file upload, a chat widget) and run through the five steps. You’ll be amazed at how many vulnerabilities you find without writing a single line of code.

Pro tip: Start a threat modeling habit. Every time you design a new feature, spend 10 minutes sketching a quick STRIDE table. The investment pays off 100x when you avoid a production incident.

Practice recap

Now test your skills: take a feature you recently shipped — or the Flask app from this lesson — and run through the five steps. Write down the assets, draw the trust boundaries, apply STRIDE to each boundary, and rank the top three threats. Share your findings with a colleague or revisit this lesson to compare your reasoning.

Common mistakes

  • Treating the threat model as a one-time meeting outcome instead of a living document — review it every time you add a feature or change infrastructure.
  • Forgetting to include third-party and supply-chain components (e.g., CDNs, payment APIs, open-source libraries) as assets or trust boundaries.
  • Listing every possible THREAT but never ranking them — without prioritization you’ll fix the wrong things.
  • Assuming the database is the only valuable asset — session tokens, API keys, and even app logic can be prime targets.

Variations

  1. Use an attack tree for a single high-value threat: start with the goal (e.g., 'steal admin credentials') and branch downward into possible methods, then mitigate each leaf.
  2. Adopt a lightweight 'security stories' approach in Agile: each user story gets a 'bad things that could happen' clause, integrating threat modeling into your daily workflow.
  3. Score each STRIDE threat with DREAD (Damage, Reproducibility, Exploitability, Affected users, Discoverability) to get a numeric priority — great for teams that like quantitative decisions.

Real-world use cases

  • A fintech startup threat-models its new payment integration before launch, uncovering an API key leak on the frontend that could have led to unauthorized transaction refunds.
  • A health-tech company uses STRIDE on a patient portal to identify a broken access control, preventing a user from viewing another patient's records.
  • A social media app threat-models its file upload feature and discovers a zip-slip vulnerability, avoiding a server takeover via compressed archives.

Key takeaways

  • A threat model forces you to think like an attacker and systematically identify assets, trust boundaries, and attack paths — before you ship.
  • STRIDE (Spoofing, Tampering, Repudiation, Information disclosure, DoS, Elevation of privilege) gives you a structured way to enumerate threats per component.
  • Severity ranking (likelihood × impact) turns a long list of threats into a short, actionable list of security fixes.
  • Fixings often involve simple coding patterns: parameterized SQL, hashing passwords, environment variables for secrets, and HTTPS everywhere.
  • Threat modeling is a continuous habit, not a one-time artifact — repeat it with every new feature or architecture change.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.