The 9 threats you'll actually see
Web security sounds scary, but most attacks fall into a handful of categories — and most fixes are a single line of code. Here's the map:
No HTTPS
Data sent in plain text can be read by anyone on the network.
XSS
Attackers inject scripts that run in your users' browsers.
SQL Injection
Attackers rewrite your database queries from a form field.
Weak Passwords
Stored plain-text or hashed with MD5 — both easily cracked.
CSRF
A malicious site submits forms to yours while the user is logged in.
Data Exposure
API keys or secrets accidentally committed to a public repo.
Broken Auth
Session tokens leak, sessions don't expire, no 2FA.
Outdated Libs
Known vulnerabilities in packages you haven't updated in years.
Missing Headers
No CSP, no HSTS — defaults leave your site exposed.
Always use HTTPS
HTTPS encrypts traffic between the browser and your server. Without it, anyone on the coffee-shop WiFi can read passwords and cookies in plain text.
The good news: it's free. Every modern host (Netlify, Vercel, GitHub Pages, Cloudflare) gives you HTTPS automatically. If your deploy doesn't have a padlock in the browser, stop and fix it before launching.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Validate inputs — twice
Never trust anything a user types. Validate on the client (for UX) AND on the server (for security). Attackers bypass client validation trivially.
<!-- Client-side: HTML5 constraints --> <input type="email" required maxlength="120" pattern="[^@]+@[^@]+"> <input type="number" min="0" max="150">
Server-side, use an allowlist (what is allowed) over a denylist (what isn't). Reject anything that doesn't match — never try to "clean" bad input.
<, {, --, $), your input validator should probably reject it unless you explicitly allow it.
Prevent XSS — escape user content
Cross-Site Scripting happens when you stick user input into your HTML without escaping. The attacker submits <script>steal()</script> and it runs in every visitor's browser.
// <img src=x onerror=alert(1)>
// renders as plain text
In templates (React, Vue, Svelte, Jinja, ERB) — use the framework's escape, not raw HTML insertion. React does it automatically. Beware of dangerouslySetInnerHTML.
Prevent SQL injection — use parameterized queries
If you build SQL by string-concatenating user input, an attacker types ' OR '1'='1 and gets everyone's data.
Every modern database driver supports parameterized queries (? placeholders, prepared statements, or an ORM). The golden rule: user data goes in arguments, never in the SQL string.
Store passwords correctly
Never store plain-text passwords. Never use MD5 or SHA-1 — they're too fast, meaning attackers can try billions per second.
Use a slow, salted hash: bcrypt, argon2, or scrypt. Every backend language has a library:
// Node.js with bcrypt const hash = await bcrypt.hash(password, 12); // store this const match = await bcrypt.compare(input, hash); // verify on login
CSRF — verify the request came from your site
Cross-Site Request Forgery: a malicious site tricks your logged-in user's browser into submitting a form to your site. Their session cookie goes with it, so your server thinks the user meant to do it.
Fix: include a CSRF token — a random value the attacker's site can't guess — in every state-changing form:
<form method="POST" action="/transfer"> <input type="hidden" name="_csrf" value="{{ csrfToken }}"> <!-- other fields --> </form>
On cookies, set SameSite=Lax or Strict. That alone blocks most CSRF attacks in modern browsers.
Never commit secrets to Git
API keys, database passwords, JWT secrets — they end up on GitHub's public index within minutes of being pushed. Bots scan for them 24/7.
- Put secrets in a
.envfile - Add
.envto.gitignore - Commit a
.env.examplewith keys only (no values) - Set real values in your hosting provider's env-var UI (Vercel, Netlify, Railway)
Add security headers
A few HTTP response headers block whole categories of attacks for free. Set them once and forget.
# Forces HTTPS for a year Strict-Transport-Security: max-age=31536000; includeSubDomains # Stops MIME-type sniffing X-Content-Type-Options: nosniff # Blocks your site being embedded in iframes (clickjacking) X-Frame-Options: SAMEORIGIN # Locks down what scripts/styles/images can load Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; # Hides where users came from Referrer-Policy: strict-origin-when-cross-origin
Check your site's score at securityheaders.com — aim for an A or higher.
Keep dependencies updated
Every npm install pulls in hundreds of packages — any one of them can ship a vulnerability. The fix is routine patching.
# Find known vulnerabilities npm audit # Auto-fix what can be fixed safely npm audit fix # See outdated packages npm outdated
GitHub's Dependabot (free) opens a PR every time one of your dependencies has a security patch. Turn it on: repo → Settings → Security.
Pre-launch security checklist
Before shipping, run through this list. 30 minutes of prevention saves months of incident response.
- HTTPS is enforced (browser padlock, no "Not secure" warning)
- All forms have client + server-side validation
- User input is escaped before rendering (no raw HTML injection)
- Database queries use parameterized / prepared statements
- Passwords are hashed with bcrypt/argon2/scrypt (never plain or MD5)
- Forms use CSRF tokens; cookies use SameSite=Lax
- No secrets in source code;
.envis gitignored - Security headers set (HSTS, CSP, X-Frame-Options)
npm auditis clean; Dependabot is on- Account lockout after failed login attempts
- Sessions expire; logout works everywhere
- Error messages don't leak internals (no stack traces to users)
Go deeper
- OWASP Top 10 — the industry's canonical list of web vulnerabilities, updated every few years
- OWASP Cheat Sheet Series — one-page fix guides for every common attack
- Security Headers scanner — grades your site's response headers
- Mozilla Observatory — broader HTTPS + headers audit with recommendations
- Have I Been Pwned — check if your emails or passwords appear in known breaches
- web.dev — Learn Privacy — Google's free course on privacy + security basics
Security is a habit, not a feature.
Every one of these fixes is a single line or config change. The hard part isn't writing them — it's remembering to add them before launch instead of after a breach.