Why security is everyone’s job
Web security is not a specialist concern that someone else handles. Every form, every rendered comment, every cookie and every API endpoint is an opportunity for a mistake that exposes your users. The cost of a security bug is unusually high: lost data, broken trust and legal exposure.
The good news is that most attacks follow a small number of patterns, and most defences are well understood. This guide covers the essentials: cross-site scripting, cross-site request forgery, CORS, secure authentication and the browser headers that enforce your rules.
The golden rule
Never trust the client. Anything that arrives from the browser can be forged: form values, headers, hidden fields, prices, user IDs and JavaScript state. Client-side validation is for user experience; the server must validate and authorize everything again.
This single principle underlies almost every defence. If you assume input is hostile and permissions must be checked server-side, most vulnerabilities disappear before they are written.
Cross-site scripting (XSS)
XSS is the most common serious web vulnerability. It happens when attacker-controlled data is treated as code. There are three broad types: stored (saved on the server and served to others), reflected (bounced back in a response) and DOM-based (introduced by client-side JavaScript).
The primary defence is output encoding: encode data for the context where it appears. In the DOM, use textContent rather than innerHTML.
// safe.js
const el = document.createElement("p");
el.textContent = userComment; // escaped
// If you truly need HTML, sanitise it
import DOMPurify from "dompurify";
el.innerHTML = DOMPurify.sanitize(userHtml);
On the server, use templating that escapes by default and never build SQL or HTML by string concatenation. For databases, always use parameterised queries or an ORM so input can never become SQL.
Content Security Policy
A Content Security Policy is a response header that tells the browser which sources a page may use. A strict policy turns many XSS bugs into harmless console errors.
Content-Security-Policy:
default-src 'self';
script-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Start with Content-Security-Policy-Report-Only to collect violations without breaking the site, then enforce. Avoid unsafe-inline and unsafe-eval where you can, and use nonces or hashes for the inline scripts you cannot remove.
Cross-site request forgery (CSRF)
CSRF tricks the browser into sending an authenticated request using the victim’s cookies. If your site uses cookie-based sessions and a state-changing endpoint accepts a simple request, an attacker’s page can trigger it.
Defences, layered:
- SameSite cookies.
SameSite=LaxorStrictstops cookies from being sent on cross-site requests in modern browsers. - Anti-CSRF tokens. A per-session token included in forms and verified on the server.
- Check the origin. Reject state-changing requests whose
Originheader is not your site. - Never mutate on GET. Use POST, PUT, PATCH or DELETE for changes.
CORS
CORS is a browser rule about which origins may read responses from your API. By default, a page cannot read a cross-origin response unless the server allows it with Access-Control-Allow-Origin.
Two things are commonly misunderstood:
- CORS protects the user’s browser, not your server. Other servers can call your API freely; only server-side authorization stops them.
- A permissive
Access-Control-Allow-Origin: *combined with credentials is not allowed, and reflecting arbitrary origins is dangerous.
Configure CORS explicitly for the origins you trust, and keep authentication and authorization on the server.
Secure authentication
Authentication is where mistakes are most costly.
- Hash passwords with bcrypt, scrypt or Argon2, never plaintext or reversible encryption.
- Use HttpOnly, Secure, SameSite cookies for sessions so JavaScript cannot read the token.
- Prefer short-lived tokens with refresh, and rotate them.
- Add multi-factor authentication for sensitive accounts.
- Rate-limit login and lock out after repeated failures.
- Never put secrets in client code — anything shipped to the browser is public.
The comparison above shows the cookie versus localStorage trade-off. localStorage tokens are convenient but readable by any script, so a single XSS steals the session. HttpOnly cookies remove that path.
Other essentials
- HTTPS everywhere, with HSTS to prevent downgrade attacks.
- Validate and encode input on the server for every field.
- Apply least privilege to database users, API keys and cloud roles.
- Set security headers: CSP, HSTS,
X-Content-Type-Options: nosniff,Referrer-Policyandframe-ancestors. - Keep dependencies patched and audit them regularly.
- Do not leak details in error messages or stack traces in production.
- Log and monitor authentication events and permission failures.
Best practices
- Treat all client input as untrusted and validate on the server.
- Encode output for its context; use
textContentand sanitise any HTML. - Use parameterised queries for all database access.
- Store sessions in HttpOnly, Secure, SameSite cookies.
- Add a Content Security Policy and start with report-only.
- Use SameSite plus anti-CSRF tokens for state-changing requests.
- Keep secrets on the server and rotate them.
Common mistakes
- Using
innerHTMLwith user data. - Putting session tokens in
localStorage. - Trusting client-side validation or hidden form fields.
- Building SQL with string concatenation.
- Assuming CORS is an access control mechanism.
- Shipping API keys in front-end code.
Where to go next
Security is a practice, not a checklist you finish. Ground it in the HTTP protocol, enforce it on the Node.js server, and handle secrets safely when you ship with Docker. Read the OWASP Top 10, then audit one form in your own app end to end — you will almost always find something worth fixing.