Skip to main content

Best practices

Use parameterized queries for all database access

Send the query structure and the user-supplied values to the database separately so input can never change the meaning of the SQL. This is the definitive defense against SQL injection and works the same way across drivers and ORMs that bind parameters.

  • Do: use a prepared statement such as SELECT * FROM users WHERE email = ? and bind the value.
  • Don't: build SQL by concatenation like "... WHERE email = '" + email + "'", even with manual escaping.

Encode output based on its context, not just at input

Apply context-aware encoding at the point where data is rendered - HTML body, HTML attribute, JavaScript, URL, and CSS each need different escaping. Sanitizing only on input is insufficient because the same value can be safe in one context and dangerous in another.

  • Do: rely on your framework's automatic binding (for example {{ value }} in Angular or JSX text) and encode per context.
  • Don't: assign untrusted data to raw sinks like element.innerHTML without sanitizing it first.

Enforce authorization on the server for every request

Re-check on the server that the authenticated principal is permitted to perform the specific action on the specific object, on every protected endpoint. Client-side checks and hidden UI elements are convenience only and provide no security.

  • Do: verify resource ownership server-side, e.g. confirm order.userId == currentUser.id before returning /orders/{id}.
  • Don't: trust a client-supplied role, tenant ID, or object ID, or assume "logged in" means "authorized".

Validate and allow-list all untrusted input on the server

Treat every input (body, query, headers, files) as hostile and validate it against a strict allow-list of expected types, ranges, and formats before use. Allow-listing what is permitted is far more robust than trying to block known-bad patterns.

  • Do: validate with a schema and reject anything that does not match, e.g. an enum of ["asc","desc"] for a sort param.
  • Don't: rely on client-side validation alone, or use deny-lists that attackers can trivially evade with new encodings.

Store passwords with a slow, salted, memory-hard hash

Use a purpose-built password hashing function so that a database breach does not yield usable credentials. The unique per-password salt defeats rainbow tables and a high work factor makes offline cracking expensive.

  • Do: hash with Argon2id (or bcrypt/scrypt) with a tuned cost factor and a unique salt per password.
  • Don't: store plaintext, use fast hashes like MD5 or SHA-256 alone, or reuse a single global salt.

Set Secure, HttpOnly, and SameSite on session cookies

Harden the session cookie so it cannot leak over plain HTTP, cannot be read by JavaScript, and is not attached on cross-site requests. These three attributes together blunt session theft via XSS and CSRF.

  • Do: issue Set-Cookie: sid=...; Secure; HttpOnly; SameSite=Lax; Path=/ and regenerate the ID on login.
  • Don't: expose the session ID to JavaScript, send it over HTTP, or keep the same ID across a privilege change.

Enforce HTTPS everywhere and send HSTS

Serve all traffic over TLS 1.3 with valid certificates and add the Strict-Transport-Security header so browsers refuse to downgrade to HTTP. This prevents eavesdropping, tampering, and SSL-stripping attacks.

  • Do: set Strict-Transport-Security: max-age=63072000; includeSubDomains; preload and redirect HTTP to HTTPS.
  • Don't: mix HTTP and HTTPS content, or rely on a redirect alone without HSTS, leaving the first request unprotected.

Ship a strict Content Security Policy

Use a nonce- or hash-based script-src so that injected inline scripts without the correct nonce cannot execute, turning many XSS payloads inert. CSP is powerful defense in depth on top of output encoding.

  • Do: send Content-Security-Policy: script-src 'self' 'nonce-'; object-src 'none'; base-uri 'self'.
  • Don't: weaken it with 'unsafe-inline' or a blanket *, which effectively disables the protection.

Require phishing-resistant MFA where it matters

Add a second factor to authentication and prefer WebAuthn/FIDO2 (passkeys, security keys) for high-value accounts, because the credential is bound to the origin and cannot be phished or replayed. This stops most account takeovers even when a password leaks.

  • Do: offer passkeys/WebAuthn and fall back to TOTP authenticator apps where needed.
  • Don't: rely on SMS one-time codes as the only second factor, since they are vulnerable to SIM-swapping.

Apply least privilege to every account and token

Grant each user, service, database account, and API token only the permissions it strictly needs, and make credentials short-lived. This contains the damage when any single component is compromised.

  • Do: give the app a DB user limited to its tables, and issue narrowly scoped, expiring API tokens.
  • Don't: connect as a database superuser, or hand out broad, long-lived, all-scope tokens "to keep things simple".

Rate-limit authentication and sensitive endpoints

Cap the number of requests per client and per account over a time window to blunt brute force, credential stuffing, and abuse. Combine throttling with clear 429 Too Many Requests responses and backoff.

  • Do: apply a token-bucket limit keyed on account and IP to /login, and add temporary lockout/backoff after repeated failures.
  • Don't: leave login, password-reset, and token endpoints unlimited, allowing unlimited guessing.

Guard outbound requests against SSRF

Validate, allow-list, and constrain any server-side request whose URL is influenced by user input, and block internal address ranges. This prevents attackers from reaching internal services or the cloud metadata endpoint.

  • Do: allow-list permitted hosts/schemes, block link-local and private ranges, and disable redirect following.
  • Don't: fetch an arbitrary user-supplied URL directly, exposing 169.254.169.254 and internal services.

Keep dependencies pinned, scanned, and inventoried

Manage third-party code as part of your attack surface: pin versions, scan for known vulnerabilities, and maintain an SBOM so you can react fast to new disclosures. For browser-loaded third-party scripts, verify integrity.

  • Do: commit a lockfile, run dependency scanning in CI, and add integrity="sha384-..." (SRI) to external