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.innerHTMLwithout 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.idbefore 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; preloadand 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.254and 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 externaltags. - Don't: float versions with
^/latestunpinned, or load third-party scripts from a CDN with no integrity check.
Fail securely and never leak sensitive details in errors
Design error handling so that failures default to denying access and the response reveals nothing useful to an attacker. Log the detailed context server-side, but return a generic message to the client.
- Do: catch exceptions, log full detail internally, and return a generic
500or a precise but non-revealing403. - Don't: expose stack traces, SQL errors, internal hostnames, or library versions in HTTP responses.