The fastest way to understand threat modeling is to work a concrete threat modeling example, so this post models one real feature, a file-upload API, from data flow diagram to a prioritized list of fixes. Threat modeling has a reputation for being an abstract, document-heavy exercise, but at its core it is four questions asked over a picture of your system: what are we building, what can go wrong, what are we going to do about it, and did we do a good job? We will answer all four for a single feature you have probably built.
The system we are modeling
Our example feature lets authenticated users upload a profile picture. The flow is ordinary, which is the point; most real vulnerabilities live in ordinary features.
[Browser] --HTTPS--> [API Gateway] --> [Upload Service] --> [Object Storage]
|
v
[Image Processor] --> [CDN]
The user selects a file, the browser POSTs it to the upload service, the service validates and stores it in object storage, an image processor generates thumbnails, and the CDN serves the result publicly.
Before hunting for threats, mark the trust boundaries: the lines where data crosses from a less-trusted zone into a more-trusted one. There are three here. The browser-to-gateway boundary (untrusted internet into your edge), the gateway-to-upload-service boundary (edge into application logic), and the processor-to-CDN boundary (private storage into public serving). Every trust boundary is a place to ask "what if the thing on the other side is hostile?"
Applying STRIDE to each boundary
STRIDE is a prompt list, one letter per threat category, that keeps you from only imagining the attacks you already know. Walking each boundary against each letter is mechanical and surprisingly productive.
Spoofing (identity). Can someone upload as another user? The gateway authenticates the request, but does the upload service re-verify the token, or does it trust a header the gateway set? If an attacker can reach the upload service directly (a misconfigured internal network, an SSRF pivot) and forge that header, they upload as anyone. Threat: identity spoofing via trusted-header assumption.
Tampering (integrity). The file itself is attacker-controlled data crossing into our system. Can a user upload something other than an image? A file named avatar.png can contain a PHP web shell, an SVG with embedded JavaScript, or a polyglot that is both a valid image and a valid script. Threat: malicious content disguised as an image.
Repudiation (auditability). If a user uploads illegal or abusive content, can we prove who did it and when? Without an audit log tying the upload to a user id, request id, and timestamp, we cannot. Threat: no non-repudiable record of uploads.
Information disclosure (confidentiality). Object storage often defaults to guessable or enumerable keys. If the storage bucket is public and keys are sequential, an attacker enumerates every user's uploads. Threat: unauthorized read via predictable object keys or a public bucket.
Denial of service (availability). What stops a user from uploading a 5 GB file, or ten thousand files, or a decompression bomb (a tiny file that expands to gigabytes when the image processor opens it)? Threat: resource exhaustion via oversized or malicious payloads.
Elevation of privilege (authorization). Does the image processor run with more privileges than it needs? If it shells out to a tool like ImageMagick to convert files, a parser vulnerability in that tool could turn "process an uploaded image" into "run a command." Threat: privilege escalation through the processing pipeline.
Turning threats into fixes
A threat list is only useful if it becomes work. Here is each threat with a concrete countermeasure.
For the spoofing threat, re-validate the auth token inside the upload service rather than trusting a gateway header, and lock down the network so the service is not directly reachable. For tampering, validate the file by content, not by extension or the Content-Type header, and re-encode images through a trusted library so that only the pixel data survives, stripping any embedded payload.
from PIL import Image
import io
def sanitize_image(raw_bytes, max_bytes=5_000_000):
if len(raw_bytes) > max_bytes:
raise ValueError("file too large")
img = Image.open(io.BytesIO(raw_bytes))
img.verify() # reject files that are not valid images
img = Image.open(io.BytesIO(raw_bytes))
out = io.BytesIO()
img.convert("RGB").save(out, format="PNG") # re-encode; drop embedded content
return out.getvalue()
For repudiation, log every upload with user id, request id, source IP, and content hash. For information disclosure, use unguessable object keys (a random UUID, not a sequential id) and keep the bucket private, serving through signed URLs. For denial of service, enforce a size limit at the edge before the body is fully read, cap concurrent processing jobs, and set decompression limits in the image library. For elevation of privilege, run the processor in a sandbox with no network and minimal filesystem access, and keep the imaging library patched, because parser CVEs in tools like ImageMagick and libraries behind them are a recurring source of this exact escalation. A continuous SCA scan tells you when the imaging dependency in your pipeline picks up a known vulnerability.
Did we do a good job?
The fourth question is a review, not an afterthought. Walk the fixes back against the threats and confirm each is genuinely addressed, then check whether the fixes introduced new flows worth modeling (signed URLs, for instance, add a key-expiry consideration). Threat modeling is iterative; you re-model when the design changes, not once at the start.
The value of working an example like this is that it is transferable. Swap the file-upload feature for whatever you are building, draw the flow, mark the boundaries, run STRIDE, and write the fixes. Our security academy has more worked models across authentication, payments, and multi-tenant data isolation.
FAQ
What is a simple threat modeling example to start with?
A single feature with clear inputs and outputs, like a file upload, a login flow, or a payment endpoint. Draw its data flow, mark where data crosses trust boundaries, and apply STRIDE at each boundary. Small and concrete beats a whole-system model you never finish.
What does STRIDE stand for?
Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, and Elevation of privilege. Each letter is a category of threat to prompt you, so you consider attacks beyond the ones already on your mind.
When should I threat model?
During design, before the architecture is fixed, because that is when changes are cheapest. Re-model whenever the design changes meaningfully, for example when you add a new trust boundary or a new external integration.
Do I need special tools to threat model?
No. A whiteboard and the four questions get you most of the value. Tools like the OWASP Threat Dragon or the Microsoft Threat Modeling Tool help document and share diagrams, but the thinking is the part that matters.