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.
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.
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 < and > 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.
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.
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.
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.
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.
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