Secure Coding: AuthN/AuthZ, Secrets, Race Conditions & Defense in Depth
Authentication vs Authorization
// BUG: checks authentication (logged in) but forgets authorization
// (does THIS user own THIS resource?) -- classic Broken Access Control
app.get('/orders/:id', requireLogin, (req, res) => {
const order = db.getOrder(req.params.id);
res.json(order); // User A can view User B's order by changing the ID
});
// FIXED: authorization check too
app.get('/orders/:id', requireLogin, (req, res) => {
const order = db.getOrder(req.params.id);
if (order.userId !== req.user.id) {
return res.status(403).send('Forbidden'); // fail CLOSED, not open
}
res.json(order);
});Passwords, Secrets & Rate Limiting
// Password storage -- slow, salted hashing (NOT plain SHA-256,
// NOT plaintext). Deliberately slow to make brute-forcing a leaked
// hash database impractical.
const hash = await bcrypt.hash(password, 12);
// Never hardcode secrets -- committed to source control, persists
// in git history indefinitely even after a later 'removal' commit
// BAD: const apiKey = 'sk_live_abc123';
// GOOD: const apiKey = process.env.API_KEY;
// Rate limiting -- makes brute-force credential attacks impractical
app.post('/login', rateLimit({ windowMs: 60_000, max: 5 }), loginHandler);Race Conditions
// VULNERABLE: check-then-act is NOT atomic -- concurrent requests
// can all pass the balance check before any deduction applies
const balance = await getBalance(accountId);
if (balance >= amount) {
await deduct(accountId, amount); // race window here
}
// SAFE: atomic operation -- a single DB statement, or a transaction
// with proper locking
await db.query(
'UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?',
[amount, accountId, amount]
);Core Principles
Least privilege: grant only the minimum access actually needed -- limits blast radius if compromised.
Defense in depth: layer multiple independent controls -- input validation AND parameterized queries AND least-privilege DB access, not just one.
Fail securely: an unexpected error in a security check should default to DENY, not accidentally allow.
Server-side validation is the only real security control -- client-side validation is a UX convenience, trivially bypassed by calling the API directly.
Don't roll your own crypto/auth -- established, scrutinized libraries have been battle-tested far beyond what a custom implementation gets.
Don't leak internal error details (stack traces, DB errors) to users -- log full detail server-side, show a generic message externally.
Scan dependencies for known CVEs -- a project's attack surface includes every library it pulls in, not just code the team wrote.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free