A 30-Minute STRIDE Pass on a Weekend Project
Most threat modelling advice assumes you have a security team, a whiteboard and a month. You have a Saturday, a half-working Go service and a vague feeling that the login page is probably fine. STRIDE scales down surprisingly well, and half an hour with one diagram finds more than a week of vague worrying.
This is a practical walk-through. The goal: finish with a short, ranked list of things to fix, not a document. I will use a made-up project so the examples are concrete.
The project we are attacking
Imagine a small self-hosted notes app. A Go server, SQLite on disk, email magic-link login, and an upload endpoint for attachments that get stored in a directory. It sits behind a reverse proxy on a cheap VPS. That is all.
Step one is to write down what you are protecting. Not "the app": the actual things someone would want.
- Other people's notes (confidentiality)
- The ability to log in as someone else
- The VPS itself, because a foothold there is worth more than any note
- Your time at 2am when it falls over
Draw one diagram, badly
You need boxes and arrows, nothing more. Processes, data stores, external actors, and the arrows for data moving between them. Trust boundaries go in as dashed lines where the level of trust changes.
[Browser] --HTTPS--> [Reverse proxy] --HTTP--> [Go server] --SQL--> [SQLite file]
^ | |
| | +--files--> [uploads/ dir]
+------------ magic link -- [Mail provider] <--SMTP-+
Trust boundaries: Browser | Proxy+Server+disk (my VPS) | Mail provider
Ten minutes is plenty. If the diagram takes longer, you are modelling the wrong level of detail. Every arrow that crosses a dashed line is where the interesting problems live.
The six questions
STRIDE is a mnemonic for six kinds of trouble. It originated at Microsoft in the late 1990s, and each letter maps to a security property you want to keep.
- Spoofing: pretending to be someone or something else (breaks authentication)
- Tampering: changing data you should not (breaks integrity)
- Repudiation: doing something and being able to deny it (breaks accountability)
- Information disclosure: reading what you should not (breaks confidentiality)
- Denial of service: making it unavailable (breaks availability)
- Elevation of privilege: doing more than you are allowed (breaks authorisation)
Now walk each arrow and box and ask all six. Most will be "not applicable, next". That is fine. The value is in the two or three that are not.
Quick detour: why per element and not per system
Asking "is my app vulnerable to spoofing?" gets you a shrug. Asking "can something pretend to be the mail provider when it calls my callback?" gets you an answer. STRIDE works because you point it at one small thing at a time. Also, some letters only make sense for some shapes: data stores rarely get spoofed, but they are very much tampered with and read.
Working through the arrows
Here is what a real pass over the diagram produces. Each finding is one line, with the letter that found it.
Browser to server: the magic link
- S: a magic-link token that is guessable or reusable lets anyone become anyone. Is it from a cryptographic random source, single-use, short-lived?
- S: does the link work from any browser? If someone forwards it by accident, it is a password in an email thread.
- D: can anyone request unlimited emails for any address? That is both a spam cannon and a bill.
- I: does the login form say "no such user" for unknown addresses? That leaks who has an account.
Already there are three things worth fixing, and we have only looked at one arrow.
Server to the uploads directory
- T and E: is the stored filename derived from user input? A name containing
../writes outside the directory. - I: is the uploads directory served directly by the proxy? Then anyone with a URL reads it, logged in or not.
- D: is there a size cap? One large upload can fill the disk, and a full disk also breaks SQLite.
- E: can an uploaded HTML file be served back with a content type that runs script on your origin?
The file-serving question deserves a moment. Serving user content from the same origin as your app means a stored script can act as the logged-in user. Serve downloads with Content-Disposition: attachment and X-Content-Type-Options: nosniff, or from a separate domain.
Server to SQLite
- T: parameterised queries everywhere? One string-built query in a search handler is enough.
- I: file permissions on the database. Is it readable by other users on the box, or included in a backup that is not encrypted?
- R: if a note is deleted, is there any record of who did it?
Proxy to server
This hop is plain HTTP on localhost, which is usually fine, but ask the spoofing question anyway. If the server trusts X-Forwarded-For or a header like X-User from anything that can reach its port, then anything that can reach the port can lie. Bind to 127.0.0.1, not 0.0.0.0, and strip those headers at the proxy.
The mail provider
This is a third party you cannot inspect. Tampering and disclosure are largely out of your hands: whoever controls the recipient's mailbox controls the account. That is worth writing down as an accepted risk, because it is a real property of email login rather than a bug you can patch.
Turn findings into a ranked list
Do not fix things in the order you found them. Score each one on two questions: how bad if it happens, and how easy is it for a stranger to try. A rough high, medium, low for each is enough.
- Path traversal in upload filenames (bad, easy). Fix today.
- Uploads served from the app origin (bad, easy). Fix today.
- Magic-link token entropy and single use (bad, moderate). Check today.
- No rate limit on login emails (moderate, easy). Fix this week.
- Missing audit trail for deletes (low, n/a). Backlog.
Anything you decide not to fix gets a one-line reason. "Accepted: email account compromise is out of scope" is a decision. Silence is just forgetting.
Pitfalls
- Do not try to be exhaustive. The pass is meant to be lopsided, catching the obvious and expensive things.
- Do not model attackers you cannot afford to defend against. A weekend project will not stop a state actor; it should stop a bored teenager with curl.
- Do not skip repudiation because it sounds like paperwork. Logs are how you discover the other five happened.
- Do not leave the diagram in your head. Commit it as a text file next to the code.
Checking it worked
A threat model has done its job if the output is changes, not notes. Each fix should have either a test or a one-line check you can repeat, such as a request with ../../etc/passwd as a filename that must fail, or a magic link used twice that must be rejected the second time.
Then set a reminder to redo the diagram the next time you add an arrow. New integration, new box, new ten minutes. The second pass is usually faster, and the diagram is already drawn.