๐ป Module 5 ยท Application TM
Threat Modeling Basic Applications
Put the process into practice on a simple, realistic application โ a task management CRUD app with user accounts. We'll build the full threat model end to end.
๐ฏ Learning Objectives
- Draw the complete DFD for a simple CRUD web app
- Identify the most common application-level threats by category
- Understand authentication and session management attack patterns
- Map OWASP top vulnerabilities to STRIDE categories
- Define mitigations for each identified threat
1The Example Application
We'll threat model "TaskFlow" โ a simple task management app. It's deliberately minimal to keep the focus on threat modeling mechanics. But it contains all the patterns you'll see in any production application.
Users
Register with email + password. Log in/out. Each user sees only their own tasks.
Tasks
CRUD operations: create, read, update, delete tasks. Tasks belong to a single user.
Notifications
Email reminders for due tasks. Uses a third-party email service (SendGrid).
Storage
PostgreSQL database. Users table and Tasks table. Hosted on a cloud VPS.
Tech Stack (for context)
Node.js + Express API ยท React frontend ยท PostgreSQL ยท JWT sessions ยท SendGrid email ยท Nginx reverse proxy ยท Hosted on a single VPS. This is typical of a startup MVP.
2The TaskFlow DFD
Let's draw the architecture. The goal is to see every data flow and every trust boundary:
TaskFlow โ Full Data Flow Diagram
TaskFlow's complete DFD โ three trust zones, three processes, two data stores, and one external entity
3Authentication Threats (Process 1)
T-01 โ Credential Stuffing [STRIDE: Spoofing]
Attacker uses credential dumps from other breaches to try username/password pairs at scale. The auth service will happily accept valid stolen credentials.
Mitigations
TOTP-based MFA ยท bcrypt/Argon2 password hashing (slow hash โ defeats stuffing rate) ยท Bot detection (CAPTCHA after 3 fails) ยท Notify user of new login location ยท Check credentials against HaveIBeenPwned API on login
T-02 โ Weak JWT Secret [STRIDE: Tampering + Elevation of Privilege]
If the JWT signing secret is too short or predictable, an attacker can brute-force it offline, then forge tokens with arbitrary claims (e.g., admin role, different user_id).
Mitigations
256-bit cryptographically random JWT secret stored in a secrets manager (never in code/env file) ยท Prefer RS256 (asymmetric) over HS256 ยท Short token expiry (15 min access + refresh token rotation)
T-03 โ JWT Stored in localStorage [STRIDE: Information Disclosure]
React SPA stores JWT in localStorage โ accessible to any JavaScript on the page. An XSS vulnerability in any dependency can steal the token and give the attacker a persistent session.
Mitigations
Store JWT in HttpOnly + Secure + SameSite=Strict cookie instead of localStorage ยท Add Content-Security-Policy headers ยท Subresource Integrity (SRI) for all third-party scripts
4Authorization Threats (Process 2 โ Tasks API)
The #1 App-Level Vulnerability: IDOR
Insecure Direct Object Reference (IDOR) is the most common authorization failure in CRUD apps. It happens when the API trusts user-supplied IDs without verifying ownership. Example:
GET /api/tasks/42 โ does the server check that task 42 belongs to the authenticated user? If not, any logged-in user can read anyone's tasks.T-04 โ IDOR: Read Other Users' Tasks [STRIDE: Info Disclosure + Elevation]
Authenticated attacker changes the task ID in the URL from their own (e.g., 42) to another user's (e.g., 43). If the API doesn't check
task.user_id === jwt.user_id, they get full read access to another user's data.// โ Vulnerable
app.get('/api/tasks/:id', auth, async (req, res) => {
const task = await db.query('SELECT * FROM tasks WHERE id = $1', [req.params.id]);
res.json(task); // Missing: check task.user_id === req.user.id
});
// โ
Fixed
app.get('/api/tasks/:id', auth, async (req, res) => {
const task = await db.query(
'SELECT * FROM tasks WHERE id = $1 AND user_id = $2',
[req.params.id, req.user.id] // Ownership enforced in query
);
if (!task) return res.status(404).json({ error: 'Not found' });
res.json(task);
});
T-05 โ SQL Injection via Task Title [STRIDE: Tampering + Info Disclosure]
If the task title field is inserted directly into a SQL query string, an attacker submits
'; DROP TABLE tasks; -- or ' OR '1'='1 to extract or destroy data.// โ Vulnerable
const query = `INSERT INTO tasks (title) VALUES ('${req.body.title}')`;
// โ
Fixed โ parameterized query
const query = await db.query('INSERT INTO tasks (title, user_id) VALUES ($1, $2)',
[req.body.title, req.user.id]);
5Data Store Threats
T-06 โ Plaintext Passwords in DB [STRIDE: Information Disclosure]
If the database is breached and passwords are stored in plaintext (or with weak MD5 hashing), every user account is immediately compromised. Attackers sell these on dark markets within hours.
Mitigations
Always hash passwords with bcrypt (cost factor 12+) or Argon2id. Never MD5, SHA-1, or SHA-256 alone โ these are too fast. Salt is automatically included in bcrypt. Store only the hash, never the plaintext.
T-07 โ DB Exposed to Internet [STRIDE: Denial of Service + Info Disclosure]
PostgreSQL running on default port 5432 exposed to the internet. Attackers scan for exposed databases continuously. Direct database attacks or brute-force connection attempts.
Mitigations
Bind PostgreSQL to localhost only (
listen_addresses = 'localhost' in postgresql.conf). Firewall rules: no inbound access on port 5432. App server connects via Unix socket or localhost only.6External Service Threats (Process 3 + SendGrid)
T-08 โ SendGrid API Key Exposure [STRIDE: Spoofing]
The SendGrid API key is hardcoded in the source code or committed to a public GitHub repo. Attacker finds it, uses it to send phishing emails from the app's domain โ destroying email reputation.
Mitigations
Store API keys in environment variables or secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.). Never commit secrets. Add
.env to .gitignore. Use git-secrets or similar pre-commit hooks to catch accidental commits.7Full Threat Table โ TaskFlow
| ID | Component | STRIDE | Threat | Risk | Decision |
|---|---|---|---|---|---|
| T-01 | Auth Service | S | Credential Stuffing | Critical | Mitigate: MFA + bot detection |
| T-02 | JWT Signing | T+E | Weak JWT secret โ token forgery | Critical | Mitigate: 256-bit secret, RS256 |
| T-03 | Frontend (SPA) | I | JWT in localStorage โ XSS theft | High | Mitigate: HttpOnly cookie |
| T-04 | Tasks API | I+E | IDOR: access other users' tasks | Critical | Mitigate: ownership check in query |
| T-05 | Tasks API | T | SQL Injection via task title | Critical | Mitigate: parameterized queries |
| T-06 | Users DB | I | Plaintext passwords | Critical | Mitigate: bcrypt/Argon2id |
| T-07 | Database | D+I | DB exposed to internet | High | Mitigate: localhost-only binding |
| T-08 | Notify โ SendGrid | S | API key hardcoded in code | High | Mitigate: secrets manager |
8Your Turn โ Spot the Threat
โ๏ธ Exercise 5.1
Identify this vulnerability
A logged-in user sends
GET /api/tasks/99. Task #99 belongs to a different user. The API returns it without error. What STRIDE category does this represent?9Module Summary
Key Takeaways
- Authentication: hash passwords (bcrypt), short JWTs, HttpOnly cookies, MFA
- Authorization: always verify ownership server-side โ IDOR is the #1 app-level failure
- SQL injection: parameterized queries โ always, no exceptions
- Secrets: never in source code โ use environment variables or secrets managers
- Network: databases should never be directly reachable from the internet