Secure Coding
02 / 02

AuthN/AuthZ, Secrets, Race Conditions & Defense in Depth

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