Learning Material

Secure Coding: Building Robust and Resilient Software

Guide · Application Security Fundamentals

Cover: Secure Coding: Building Robust and Resilient Software

Most vulnerabilities aren't exotic

The realistic threat to most applications isn't a nation-state attacker — it's an automated scanner finding one of a handful of well-known, well-documented mistakes. The good news: the same handful of defensive habits closes most of them.

Injection: never build queries from raw input

SQL injection happens when user input gets concatenated directly into a query string, letting an attacker inject their own SQL:

// Vulnerable — user input becomes part of the SQL itself
db.query("SELECT * FROM users WHERE email = '" + email + "'");

// Safe — the value is bound as a parameter, never interpreted as SQL
db.query("SELECT * FROM users WHERE email = ?", [email]);

Parameterized queries (or an ORM that generates them for you) close this entirely — the database engine treats the parameter as pure data, never as part of the query structure, no matter what characters it contains.

Cross-site scripting (XSS): escape output, not just input

If user-submitted content ever gets inserted into a page as raw HTML, an attacker can submit a <script> tag and have it execute in every other visitor's browser. The fix isn't filtering input on the way in — it's escaping on the way out, at render time, so < and > become &lt; and &gt; and can never be interpreted as a tag. Every modern templating system does this by default; the risk is almost always a deliberate opt-out (React's dangerouslySetInnerHTML, for example) used without realizing what it disables.

Authentication: hash, never encrypt, passwords

Encryption is reversible by design — anyone with the key gets the plaintext back. Password hashing (bcrypt, scrypt, or Argon2) is deliberately one-way and slow, so that even if your database leaks, recovering the original passwords requires enormous computing time per guess. Always include a per-user random salt, so two users with the same password don't produce the same hash — otherwise a single cracked hash reveals every account using that password.

Authorization: check on every request, not just at login

A user being logged in tells you who they are — it doesn't tell you what they're allowed to touch. A common real-world gap: an endpoint like /api/orders/482 checks that you're logged in, but never checks that order 482 actually belongs to you. Every request that touches a specific resource needs its own ownership or permission check, not just a "logged in or not" gate at the door.

Secrets don't belong in source code

API keys, database passwords, and signing secrets committed to a Git repository stay in that repository's history forever, even after being "removed" in a later commit. Load them from environment variables instead, keep the file that holds real values out of version control, and rotate any credential immediately if it's ever been committed by mistake.

Rate limiting: assume automation, not just individual bad actors

A login form with no rate limit isn't just vulnerable to a human trying a few passwords — it's vulnerable to a script trying thousands per minute. Limiting sensitive endpoints (login, password reset, account creation) to a reasonable number of attempts per IP per time window closes off brute-force attacks almost entirely, at minimal cost to legitimate users.

The mindset that actually matters

Secure coding isn't a checklist you complete once — it's asking "what happens if this input is malicious, missing, or unexpectedly huge" as a routine part of writing any code that touches user input, not an afterthought applied right before launch.

Comments