Why Security Is Every Developer's Job
It is tempting to think security belongs to a dedicated team or to "the ops people." In reality, the most common breaches do not come from clever zero-day exploits — they come from ordinary application bugs: an unescaped output, a missing permission check, a hardcoded password. Those bugs live in the code you write every day.
The good news is that the majority of real-world attacks fall into a small number of well-understood categories. Once you recognize them, defending against them becomes a set of repeatable habits rather than a mysterious art. Treat security like accessibility or performance: a quality you build in from the first line, not a patch you scramble for after launch.
The mindset shift: Assume every input is hostile, every network is being watched, and every user might be an attacker. Defensive code is not paranoid — it is professional.
HTTPS & Encryption
HTTPS encrypts the connection between the browser and your server using TLS. Without it, anyone on the same network — a public Wi-Fi router, an ISP, a malicious proxy — can read and even modify the traffic in transit. That includes passwords, session cookies, and personal data sent in plain text.
What HTTPS protects you from
- Eavesdropping: attackers sniffing credentials and tokens off the wire.
- Tampering: injecting malicious scripts or ads into your pages mid-transit.
- Impersonation: the TLS certificate proves the user is really talking to your server.
Getting a certificate is free and automated today (for example, via Let's Encrypt). Once HTTPS is active, tell browsers to always prefer it with the HSTS header:
// Force HTTPS for one year, including subdomains
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadTip: Redirect all HTTP traffic to HTTPS and mark every cookie as Secure so it is only ever sent over an encrypted connection. There is no good reason to serve a modern site over plain HTTP.
Common Attacks: XSS, SQL Injection & CSRF
Three attacks show up again and again in vulnerable web apps. Each is easy to understand and, once understood, straightforward to prevent.
Cross-Site Scripting (XSS)
XSS happens when an attacker manages to inject JavaScript into a page that other users then run in their browsers. If a comment field stores <script>...</script> and you render it back unescaped, every visitor runs the attacker's code — which can steal cookies or hijack sessions.
// VULNERABLE — raw user content injected into the DOM
element.innerHTML = userComment;
// SAFE — treat user content as text, never as markup
element.textContent = userComment;The fix is to escape output based on context and to set a Content Security Policy (CSP) header that restricts which scripts may run. Modern frameworks escape values in templates by default — the danger is when you reach around them with raw HTML.
SQL Injection
SQL injection occurs when user input is concatenated directly into a database query. An attacker can then rewrite the query — reading other users' data or dropping tables entirely.
// VULNERABLE — input glued straight into SQL
const q = "SELECT * FROM users WHERE email = '" + email + "'";
// Input: ' OR '1'='1 -> returns every row
// SAFE — parameterized query, input is data not code
db.query("SELECT * FROM users WHERE email = ?", [email]);Always use parameterized queries (prepared statements) or a trusted query builder. Never build SQL by string concatenation, no matter how "clean" the input looks.
Cross-Site Request Forgery (CSRF)
CSRF tricks a logged-in user's browser into making an unwanted request to your site. Because the browser automatically attaches the user's session cookie, the request looks legitimate. A hidden form on a malicious page could, for example, transfer money or change an email address.
The standard defenses are anti-CSRF tokens (a secret value tied to the session that must accompany state-changing requests) and the SameSite cookie attribute, which stops cookies from being sent on cross-site requests.
// Block cross-site cookie sending by default
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=StrictInput Validation & Sanitization
Validation and sanitization are related but distinct. Validation rejects data that does not fit the expected shape (an email that is not an email, an age that is negative). Sanitization cleans or neutralizes data before it is used or stored (stripping dangerous HTML tags).
| Concept | Goal | Example |
|---|---|---|
| Validation | Reject malformed input outright | Require a valid email format and a positive integer for quantity |
| Sanitization | Clean input that is allowed but risky | Strip <script> tags from rich-text comments |
| Output encoding | Render data safely for its context | HTML-escape before inserting into a page |
| Allowlisting | Accept only known-good values | Restrict a sort field to name or date only |
Prefer allowlists over blocklists. Trying to enumerate every bad character is a losing game — attackers are creative. Instead, define exactly what is acceptable and reject everything else.
Secure Authentication & Password Hashing
Passwords must never be stored in plain text, and they must never be stored with fast or reversible methods. If your database leaks — and you should plan as if it will — properly hashed passwords remain extremely expensive to crack.
Hash, don't encrypt
Use a slow, salted, password-specific hashing algorithm such as bcrypt, scrypt, or Argon2. The slowness is the point: it makes brute-force guessing impractical.
// Hashing a password with bcrypt (Node.js)
const bcrypt = require('bcrypt');
// On sign-up: salt + hash, store only the hash
const hash = await bcrypt.hash(password, 12);
// On login: compare against the stored hash
const ok = await bcrypt.compare(password, hash);| Approach | Reversible? | Use for passwords? |
|---|---|---|
| Plain text | N/A | Never — a catastrophic mistake |
| MD5 / SHA-256 (fast hash) | No, but too fast | No — brute-forced in seconds |
| Encryption (AES) | Yes — key unlocks it | No — defeats the purpose |
| bcrypt / Argon2 (slow hash) | No | Yes — the correct choice |
Beyond hashing, enforce reasonable password length, support multi-factor authentication, rate-limit login attempts to slow brute-force attacks, and store session identifiers in HttpOnly cookies so JavaScript cannot read them.
Never Trust the Client
Anything that runs in the browser can be inspected, modified, or replayed. JavaScript can be edited in dev tools, hidden form fields can be changed, and API requests can be crafted by hand with any tool. Client-side validation is a convenience for honest users — it improves the experience, but it is never a security control.
// Client-side check = nice UX, NOT security
if (price < 0) showError(); // attacker just skips this
// The server must re-check EVERYTHING
if (price < 0) return res.status(400).send('Invalid price');
// Also verify the user is allowed to perform this actionGolden rule: every authorization and validation check that matters must be enforced again on the server. Assume the client has been tampered with.
Secrets & Environment Variables
API keys, database passwords, and signing secrets must never be committed to your repository or shipped to the browser. A leaked key in public source control can be scraped within minutes of being pushed.
Keep secrets in environment variables loaded at runtime, and keep the file that holds them out of version control:
# .env (add this file to .gitignore — never commit it)
DATABASE_URL=postgres://user:pass@host:5432/app
SESSION_SECRET=long-random-value
STRIPE_SECRET_KEY=sk_live_xxx
# .gitignore
.envTip: Commit a .env.example with the variable names but no real values, so teammates know what to configure. Rotate any secret the moment you suspect it has leaked.
A Practical Security Checklist
Run through this list before shipping any web app. None of these items requires deep security expertise — just discipline.
- Serve everything over HTTPS and enable HSTS.
- Use parameterized queries for every database call.
- Escape output and set a Content Security Policy to block XSS.
- Add CSRF tokens and
SameSitecookies for state-changing requests. - Validate and sanitize all input with allowlists on the server.
- Hash passwords with bcrypt or Argon2; never store them in plain text.
- Mark cookies
HttpOnlyandSecure; rate-limit login attempts. - Re-check authorization on the server for every protected action.
- Keep secrets in environment variables, never in source control.
- Keep dependencies updated and audit them for known vulnerabilities.
Remember: security is a habit, not a feature. Build these checks into your normal workflow and revisit them as your app grows — small, consistent diligence beats a last-minute audit every time.