Security

Practical Application Security for Engineers Who Ship

Threat modeling, secrets, and the small number of controls that stop the majority of real attacks. Security that fits into how software actually gets built, not a compliance checklist.

Abstract secure circuit pattern representing application security

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.

ControlStops
Parameterized queries everywhereSQL injection
Output encoding by defaultCross-site scripting (XSS)
Short-lived, scoped credentialsBlast radius of a leaked secret
MFA on everything privilegedCredential stuffing, phishing
Dependency + patch automationKnown-CVE exploitation
High-leverage controls versus the attacks they prevent.

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.

least-privilege.ts ts
// 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.

Continue reading