courses

Threat Modeling

Finding the flaws before you build them

Threat modeling is a structured process for identifying security threats to a system, understanding their impact, and determining mitigations — during design, before code is written. It is one of the highest-leverage security activities there is: finding a design-level vulnerability during planning costs far less to fix than discovering it in production.

ℹ️ Threat modeling is where adversary models on the offense side meet the controls on the defense side: you enumerate what an attacker could do, then decide — per threat — to mitigate, accept, or transfer the risk. It feeds directly into vulnerability management and the rest of the secure SDLC.

What is Threat Modeling?

Threat modeling answers four questions (per the OWASP Threat Modeling Manifesto):

  1. What are we building? — understand the system through diagrams and documentation
  2. What can go wrong? — identify threats systematically
  3. What are we going to do about it? — decide on mitigations or accepted risks
  4. Did we do a good enough job? — validate that the model is complete and mitigations work

Threat modeling is not a one-time activity. It should be revisited when the architecture changes, new features are added, or new threat intelligence emerges.

When to threat model

STRIDE

STRIDE is the most widely used threat categorization framework, developed at Microsoft. Each letter represents a threat category:

Letter Threat Violated property Example
S Spoofing Authentication Attacker impersonates a legitimate user or service
T Tampering Integrity Attacker modifies data in transit or at rest
R Repudiation Non-repudiation User denies performing an action; no audit log exists
I Information Disclosure Confidentiality Sensitive data exposed via error messages or misconfiguration
D Denial of Service Availability Service made unavailable through resource exhaustion
E Elevation of Privilege Authorization Attacker gains higher permissions than intended

Applying STRIDE to a component

For each component or data flow in a system diagram, ask which STRIDE threats apply:

Example: REST API endpoint POST /api/v1/transfer (bank transfer)

S — Spoofing:
    Threat: Attacker forges authentication token to impersonate a user
    Mitigation: Validate JWT signature; use short-lived tokens; enforce MFA for transfers

T — Tampering:
    Threat: Attacker modifies transfer amount in transit
    Mitigation: TLS for all traffic; HMAC on request body; idempotency keys

R — Repudiation:
    Threat: User claims they did not initiate a transfer
    Mitigation: Audit log with timestamp, user ID, IP, and request body hash

I — Information Disclosure:
    Threat: Error messages reveal account balance or SQL query structure
    Mitigation: Generic error responses; structured error codes; no stack traces in production

D — Denial of Service:
    Threat: Attacker floods endpoint to exhaust transfer processing capacity
    Mitigation: Rate limiting per user; CAPTCHA for high-value operations; circuit breaker

E — Elevation of Privilege:
    Threat: Regular user accesses admin transfer approval endpoint
    Mitigation: Role-based authorization check on every endpoint; integration test coverage

STRIDE threat modeling workflow

# Python: simple threat model data structure
from dataclasses import dataclass, field
from enum import Enum
from typing import List

class StrideCategory(Enum):
    SPOOFING = "Spoofing"
    TAMPERING = "Tampering"
    REPUDIATION = "Repudiation"
    INFORMATION_DISCLOSURE = "Information Disclosure"
    DENIAL_OF_SERVICE = "Denial of Service"
    ELEVATION_OF_PRIVILEGE = "Elevation of Privilege"

@dataclass
class Threat:
    category: StrideCategory
    component: str
    description: str
    mitigation: str
    risk_rating: str    # Critical / High / Medium / Low
    status: str         # Open / Mitigated / Accepted

@dataclass
class ThreatModel:
    system_name: str
    threats: List[Threat] = field(default_factory=list)

    def add_threat(self, threat: Threat):
        self.threats.append(threat)

    def open_threats(self) -> List[Threat]:
        return [t for t in self.threats if t.status == "Open"]

    def summary(self):
        by_rating = {}
        for t in self.open_threats():
            by_rating.setdefault(t.risk_rating, []).append(t)
        for rating in ['Critical', 'High', 'Medium', 'Low']:
            items = by_rating.get(rating, [])
            if items:
                print(f"{rating}: {len(items)} open threats")

model = ThreatModel("Payment API")
model.add_threat(Threat(
    category=StrideCategory.SPOOFING,
    component="POST /api/v1/transfer",
    description="Attacker forges JWT to impersonate account holder",
    mitigation="Validate RS256 JWT signature against JWKS endpoint; enforce exp claim",
    risk_rating="Critical",
    status="Open",
))
model.summary()

PASTA

PASTA (Process for Attack Simulation and Threat Analysis) is a risk-centric threat modeling methodology with seven stages. Where STRIDE is threat-centric (what can go wrong?), PASTA is attacker-centric (what would an attacker do?).

PASTA stages

Stage Name Key activities
1 Define Objectives Business and security objectives; regulatory requirements
2 Define Technical Scope System components, dependencies, interfaces
3 Application Decomposition Data flows, trust boundaries, entry points
4 Threat Analysis Threat intelligence, attacker profiles, TTPs
5 Vulnerability Analysis SAST/DAST results, CVEs in dependencies, configuration issues
6 Attack Modeling Attack trees, attack scenarios correlated to vulnerabilities
7 Risk and Impact Analysis Business impact quantification, residual risk, remediation priority

Attack trees

Attack trees decompose an attacker’s goal into sub-goals, bottom-up, identifying possible paths:

Goal: Exfiltrate customer PII from the database

OR
├── Path 1: Exploit SQL injection in search endpoint
│   AND
│   ├── Find injectable parameter (automated scanner)
│   └── Craft payload to dump users table
│
├── Path 2: Compromise a developer account
│   AND
│   ├── Phish developer credentials
│   └── Use credentials to access production database via VPN
│
└── Path 3: Exploit misconfigured cloud storage
    AND
    ├── Enumerate S3 buckets via company name guessing
    └── Access publicly readable bucket containing database backup
# Python: represent an attack tree
from dataclasses import dataclass, field
from typing import List, Optional
from enum import Enum

class NodeType(Enum):
    OR = "OR"     # any child being true makes parent true
    AND = "AND"   # all children must be true

@dataclass
class AttackNode:
    description: str
    node_type: NodeType = NodeType.OR
    likelihood: str = "Medium"   # High / Medium / Low
    children: List["AttackNode"] = field(default_factory=list)

    def is_feasible(self) -> bool:
        if not self.children:
            return self.likelihood in ("High", "Medium")
        if self.node_type == NodeType.OR:
            return any(c.is_feasible() for c in self.children)
        return all(c.is_feasible() for c in self.children)

root = AttackNode(
    "Exfiltrate customer PII",
    NodeType.OR,
    children=[
        AttackNode("SQL injection in search", NodeType.AND, children=[
            AttackNode("Find injectable parameter", likelihood="High"),
            AttackNode("Dump users table via UNION", likelihood="High"),
        ]),
        AttackNode("Compromise developer account", NodeType.AND, children=[
            AttackNode("Phish developer credentials", likelihood="Medium"),
            AttackNode("Use credentials without MFA", likelihood="Low"),
        ]),
    ]
)

print(f"Attack feasible: {root.is_feasible()}")

Threat Modeling Workflows

Data Flow Diagrams

A Data Flow Diagram (DFD) is the standard artifact for threat modeling. It shows how data moves through a system and where trust boundaries exist.

DFD elements:

Symbol Represents Threat modeling focus
Rectangle External entity (user, external system) Spoofing entry points
Rounded rectangle Process Business logic vulnerabilities
Open rectangle Data store (database, file, cache) Data at rest, access controls
Arrow Data flow Data in transit, tampering
Dashed line Trust boundary Where authentication/authorization is enforced
# Generate DFDs with draw.io (CLI export)
# Or use the pytm library for programmatic threat modeling

pip install pytm

# pytm generates DFDs and STRIDE threat lists from Python code
# save as payment_model.py, then run:
#   python3 payment_model.py --list        # print STRIDE threat list
#   python3 payment_model.py --dfd         # generate DFD (requires graphviz)
#   python3 payment_model.py --json out.json  # machine-readable output

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

tm = TM("Payment API Threat Model")
tm.description = "Threat model for the payment processing API"
tm.isOrdered = True

# Boundaries
internet = Boundary("Internet")
dmz = Boundary("DMZ")
internal = Boundary("Internal Network")

# Components
user = Actor("Customer")
user.inBoundary = internet

api = Server("Payment API")
api.inBoundary = dmz
api.usesSessionTokens = True   # enables session-hijacking threats
api.minTLSVersion = TLSVersion.TLSv12  # triggers outdated-TLS threats below this

database = Datastore("PostgreSQL")
database.inBoundary = internal
database.isSQL = True          # enables SQL injection threat rules
database.storesSensitiveData = True  # enables data-exposure threat rules

# Data flows
Dataflow(user, api, "HTTPS POST /api/v1/transfer")
Dataflow(api, database, "Parameterized SQL query")
Dataflow(database, api, "Query results")
Dataflow(api, user, "HTTPS JSON response")

tm.process()   # reads sys.argv; behaviour depends on the flag passed above

Threat Modeling in the SDLC

Sprint-level threat modeling (lightweight):

For each user story touching a security boundary:
1. Draw a mini-DFD (5 minutes, on whiteboard)
2. Ask STRIDE questions for each component and data flow
3. Add security acceptance criteria to the story
4. Create security tasks for mitigations
5. Add automated test cases to verify mitigations

Design-level threat modeling (comprehensive):

1. Kick-off meeting: architect, developers, security engineer
2. System decomposition: draw DFD with all trust boundaries
3. STRIDE analysis: one pass per component and data flow
4. Risk rating: DREAD or CVSS score each threat
5. Mitigation planning: assign owners and sprints
6. Documentation: store model in the repository alongside architecture docs
7. Review trigger: model must be updated when architecture changes

Threat modeling tools

Tool Type Notes
Microsoft Threat Modeling Tool Desktop (Windows) STRIDE-based; generates reports; free
OWASP Threat Dragon Web / Desktop Open-source; STRIDE; DFD editor
IriusRisk SaaS Enterprise; integrates with Jira
pytm Python library Code-based; generates DFDs and threat lists
Threagile Go-based YAML Agile threat modeling as code

Attack Surface Mapping

The attack surface is the sum of all points where an attacker could try to enter or extract data from a system. Reducing the attack surface is one of the most effective security strategies.

Identifying the attack surface

# Network-level: discover open ports and services
nmap -sV -sC -p- --open 10.0.1.0/24

# Web application: enumerate endpoints
# Target: the DVWA instance running on ubuntu-server in the course lab (10.10.10.20)
# Using gobuster for directory/endpoint discovery
gobuster dir -u http://10.10.10.20/ -w /usr/share/wordlists/dirb/common.txt

# Using ffuf to fuzz for DVWA paths
ffuf -u http://10.10.10.20/dvwa/vulnerabilities/FUZZ -w /usr/share/wordlists/dirb/common.txt

# Cloud: enumerate public S3 buckets
python3 -c "
import boto3
s3 = boto3.client('s3', region_name='us-east-1')
for bucket in s3.list_buckets()['Buckets']:
    try:
        acl = s3.get_bucket_acl(Bucket=bucket['Name'])
        for grant in acl['Grants']:
            if grant['Grantee'].get('URI','').endswith('AllUsers'):
                print(f\"PUBLIC: {bucket['Name']}\")
    except Exception as e:
        pass  # bucket not accessible or error
"

# Kubernetes: enumerate exposed services
kubectl get services -A | grep -v ClusterIP   # find NodePort and LoadBalancer services
kubectl get ingress -A                        # find externally accessible ingresses

Attack surface reduction

# Disable unnecessary services
systemctl list-units --type=service --state=running   # audit running services
systemctl disable --now cups bluetooth avahi-daemon   # disable unneeded services

# Remove unnecessary packages
apt list --installed 2>/dev/null | grep -v automatic  # review installed packages
apt purge telnet ftp rsh-client rsh-server            # remove legacy insecure tools

# Restrict network access
# Allow only what is needed; deny everything else
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT

Risk Quantification

Not all threats are equally important. Risk quantification helps prioritize which threats to address first.

DREAD scoring

DREAD is a risk rating model with five factors (each scored 1–3):

Factor 1 (Low) 2 (Medium) 3 (High)
Damage Minor information leak Non-sensitive data loss Complete system compromise
Reproducibility Requires special conditions Reproducible occasionally Always reproducible
Exploitability Requires expert attacker Some skill required Script kiddie / automated
Affected users Single user Group of users All users
Discoverability Requires source access Published tool/technique Widely known
from dataclasses import dataclass

@dataclass
class DREADScore:
    damage: int          # 1-3
    reproducibility: int
    exploitability: int
    affected_users: int
    discoverability: int

    def total(self) -> int:
        return self.damage + self.reproducibility + self.exploitability \
               + self.affected_users + self.discoverability

    def rating(self) -> str:
        score = self.total()
        if score >= 12:
            return "Critical"
        if score >= 8:
            return "High"
        if score >= 5:
            return "Medium"
        return "Low"

sqli = DREADScore(damage=3, reproducibility=3, exploitability=2,
                  affected_users=3, discoverability=2)
print(f"SQL injection: {sqli.total()}/15 — {sqli.rating()}")
# SQL injection: 13/15 — Critical

CVSS (Common Vulnerability Scoring System)

CVSS v3.1 remains the most widely used vulnerability scoring system in CVE databases (NVD, vendor advisories). CVSS v4.0 was published in late 2023 and is now appearing alongside v3.1, so expect to see both scores on newer CVEs:

# Parse CVSS scores from Trivy JSON output
import json
import subprocess

result = subprocess.run(
    ['trivy', 'image', '--format', 'json', 'nginx:latest'],
    capture_output=True, text=True
)
data = json.loads(result.stdout)

critical_vulns = []
for result in data.get('Results', []):
    for vuln in result.get('Vulnerabilities', []):
        score = vuln.get('CVSS', {}).get('nvd', {}).get('V3Score', 0)
        if score >= 9.0:
            critical_vulns.append({
                'id': vuln['VulnerabilityID'],
                'package': vuln['PkgName'],
                'score': score,
                'description': vuln.get('Description', '')[:100],
            })

for v in sorted(critical_vulns, key=lambda x: x['score'], reverse=True):
    print(f"{v['score']:.1f} {v['id']} ({v['package']}): {v['description']}")

Key takeaways

References


Related course pages: DevSecOps Fundamentals · Vulnerability Management · Incident Response

🛠️ Maintenance note: scoring systems move — DREAD has fallen out of favor at Microsoft, so treat it as illustrative rather than current best practice, and watch CVSS v4.0 adoption grow relative to v3.1. Verify the pytm API (class names like Server/Datastore/Boundary) against the installed version, and re-check the Microsoft Threat Modeling Tool link each term.