Security is not a feature you add
There is no amount of hardening that makes an application "secure". There is a set of things worth protecting, a set of people who might want them, and a set of ways in. A threat model is the half-hour where you write those three lists down — and it is the difference between spending a week on the thing that matters and a week on the thing that was easiest to read about.
The four questions
- What are we building? A drawing of the pieces: the browser, your server, the database, the payment provider, the email service. Every arrow between two boxes is a place data crosses a boundary.
- What can go wrong? For each box and arrow: what would an attacker do? Read something they should not, change something, pretend to be somebody, stop it working, or get in and stay in.
- What are we going to do about it? Per risk: fix it, reduce it, accept it, or move it to someone who does it better (a payment provider, an identity provider).
- Did we do a good job? Write it down, re-read it when the architecture changes, and keep the "accepted" list honest.
Assets, and being specific
"Protect user data" is not a plan. "An attacker with a stolen session cookie can read another customer's order history through /api/orders/:id" is a plan, because it names the fix.
| Asset | What "compromised" means | Worth |
|---|---|---|
| Customer records | names, addresses, order history leak | reputation, a regulator, every customer |
| Session cookies | an attacker acts as a user | anything that user can do |
| The admin panel | an attacker acts as you | everything |
| The API key for the mail service | phishing from your domain | your domain’s reputation |
| Uptime | the shop is closed | revenue per hour |
Trust boundaries
browser ──► your server every field, header, cookie and URL is attacker-controlled
server ──► database everything must be parameterised
server ──► another API their response can be hostile too
server ──► browser everything you send back is escaped for where it lands
browser ──► browser another site's page can make requests to yours
Validation in the browser is a courtesy to the user. Every check that matters happens on the server, because the request can be made without your page: curl, a replayed request, a modified app. Anything else is the same mistake wearing a different hat.
Who is actually attacking you
Almost every real incident at a small company comes from the first two rows: a known vulnerability in an old dependency, an exposed admin route, a leaked key in a repository, a password reused from another breach. The exotic attacks are real and they are not your first problem.
| Who | Wants | Effort they will spend |
|---|---|---|
| Automated scanners | anything at all, at scale | seconds — they find the default admin password |
| An opportunist | your data, your compute, your domain | hours, if it looks easy |
| A competitor or a scraper | your catalogue, your prices | days, and they will read your API |
| An angry insider | specific damage | they already have credentials |
| A targeted attacker | you specifically | as long as it takes |
Defence in depth, and failing safely
One control will eventually fail, so the question is what happens next. Escaping stops XSS; CSP limits what an escape you missed can do. A parameterised query stops injection; a database user with read-only access limits what a missed one reaches. A session cookie is HttpOnly; it is also short-lived and bound to a device.
And when something fails, it should fail closed: an error in the permission check means no access, not access. A surprising number of breaches are a try/catch that swallowed the check and carried on.