Security has an image problem among engineers: it reads as a compliance tax, a checklist someone else enforces. But most real breaches do not come from exotic attacks; they come from a short list of boring mistakes. Fix those, and you have closed the door on the majority of what actually happens.
Threat model in ten minutes
You do not need a formal methodology to get most of the value. You need to answer four questions honestly for the thing you are building.
What are you protecting?
User data, money, availability, reputation. Name the assets explicitly.
Who wants it?
Opportunistic bots, motivated individuals, insiders. Different attackers, different defenses.
How would they get in?
Walk the entry points: every input, every dependency, every trust boundary.
What happens if they do?
Blast radius. A leaked marketing email list and a leaked password database are not the same emergency.
The controls that actually matter
If you did nothing else, these few controls would stop a startling fraction of real-world attacks.
| Control | Stops |
|---|---|
| Parameterized queries everywhere | SQL injection |
| Output encoding by default | Cross-site scripting (XSS) |
| Short-lived, scoped credentials | Blast radius of a leaked secret |
| MFA on everything privileged | Credential stuffing, phishing |
| Dependency + patch automation | Known-CVE exploitation |
Notice what is not on the list: nothing exotic. The attacks that succeed at scale are overwhelmingly the well-understood ones against systems that skipped the basics.
Secrets: assume they will leak
Every secret you create is a secret you can leak: in a log line, a stack trace, a git history, a screenshot. The goal is not perfect secrecy, which is unachievable. It is limiting what a leaked secret can do and how long it works.
// Bad: one long-lived key with god-mode access, shared everywhere.
const db = connect(process.env.ADMIN_DB_URL);
// Better: short-lived, scoped credentials minted per task.
const creds = await vault.issue({
role: 'report-reader', // read-only, one schema
ttl: '15m', // expires fast, limits the window
});
const db = connect(creds.url); Short-lived and narrowly-scoped credentials turn a catastrophic leak into a contained, expiring one. That single shift, from static admin keys to minted, scoped, expiring ones, closes an enormous category of incidents.
Validate input, encode output
Almost every injection vulnerability is a failure to keep data and code separate. Two habits handle most of it:
- Validate input at the boundary: reject anything that does not match an expected shape, and do it with an allowlist, not a blocklist.
- Encode output for its destination: HTML, SQL, shell, URL. The same string is safe in one context and dangerous in another.
Make the secure path the easy path
The durable win is not a policy document. It is making the secure way the path of least resistance: a query builder that parameterizes automatically, a secrets manager that is easier than an environment variable, a CI check that blocks a vulnerable dependency before it merges.
Developers do not choose insecure defaults out of malice. They choose whatever is easiest. Your job is to make the safe thing the easy thing.
Security is not a separate discipline bolted on at the end. It is a property of systems designed by people who spent ten minutes thinking about how things break, and who made the right way the obvious way for everyone who came after.