Trust nothing that arrived over the network
Every injection vulnerability is the same mistake: data supplied by a user was interpreted as instructions by something downstream. SQL injection, command injection, cross-site scripting and template injection are one bug wearing four costumes.
The fix is also one fix: never assemble instructions by concatenating strings. Use the mechanism that keeps data and code separate — parameterised queries for databases, argument arrays for processes, and DOM APIs that set text rather than markup.
Same mistake, two contexts
// Wrong: the value becomes markup.
element.innerHTML = user.displayName;
// Right: the value stays a value.
element.textContent = user.displayName;
-- Wrong: the value becomes SQL.
-- "SELECT * FROM users WHERE email = '" + input + "'"
-- Right: the value stays a parameter.
SELECT * FROM users WHERE email = @email;
Validate at the boundary, encode at the destination
These are different jobs and are frequently confused. Validation rejects input that does not match what you expect — a length, a format, a value from a fixed list. Encoding transforms data so that it is safe in a particular destination, and the correct encoding differs between HTML, an HTML attribute, a URL and a JavaScript string.
Validation alone is not sufficient, because plenty of legitimate input contains characters that are dangerous somewhere. Encoding alone is not sufficient, because it does not stop an eight-megabyte "postcode" from reaching your database.
Note
Client-side validation is a usability feature. It tells honest users about mistakes sooner. It provides no security whatsoever, because the client is under the attacker's control. Every check that matters must be repeated on the server.
Authentication and session handling
Do not build this. Use a well-maintained identity provider and spend the time you save on configuring it properly. If you must handle credentials directly, the minimum is:
- a modern password hashing function with a per-user salt and a cost parameter you have actually tuned;
- no upper bound on password length below about 64 characters, and no composition rules that push users towards predictable substitutions;
- session cookies marked
HttpOnly,SecureandSameSite; - session rotation on privilege change, and genuine invalidation on logout rather than merely deleting the cookie;
- rate limiting on the login endpoint, and identical responses for "unknown user" and "wrong password".
Authorisation is checked per request
Hiding a button is not authorisation. The most common serious finding in our assessments is an endpoint that checks whether you are logged in but not whether the record belongs to you. Changing an identifier in a URL then returns somebody else's data.
Check ownership on every request, in the data access layer rather than the controller, and prefer identifiers that are not sequential so that enumeration is at least not trivial. Non-sequential identifiers are a mitigation, not a control; the ownership check is the control.
Response headers do real work
A small set of headers removes whole categories of attack, costs nothing at runtime and is frequently absent.
| Header | Effect |
|---|---|
| Content-Security-Policy | Restricts where scripts, styles and images may load from; the strongest defence against cross-site scripting |
| Strict-Transport-Security | Forces HTTPS for subsequent visits, closing the first-request downgrade window |
| X-Content-Type-Options | Stops the browser guessing a content type that differs from the one you declared |
| Referrer-Policy | Prevents internal URLs leaking to third parties through the referrer |
| Permissions-Policy | Disables device APIs the site does not use |
Watch out
A Content Security Policy containing unsafe-inline for scripts provides
very little protection. If inline handlers are the obstacle, move them into a file;
that refactor is the point of the exercise, not an obstacle to it.
Dependencies are your attack surface
Most applications contain far more third-party code than first-party code, and each package is a supply chain you have implicitly accepted. Keep an inventory, patch on a schedule rather than on alarm, pin versions so builds are reproducible, and treat the number of dependencies as a cost rather than a neutral fact.
The strongest version of this control is to have fewer. A site built from HTML, CSS and vanilla JavaScript has no dependency tree to audit, which is not the right answer for every application but is very often the right answer for a website.
Log enough to reconstruct events
Log authentication outcomes, authorisation failures, administrative actions and input validation rejections. Do not log credentials, tokens, card numbers or full personal records — logs are copied, shipped and retained far more widely than the database they came from, and a log file is a common route by which sensitive data escapes.
Where to start if the list feels long
In order: get HTTPS everywhere with HSTS; add a real Content Security Policy; move authentication to a provider; add ownership checks to every data-returning endpoint; take an inventory of dependencies. Those five, done properly, close the great majority of what we find.