Not the exotic vulnerabilities — the ordinary ones. Prepared statements, CSRF tokens, session handling and file uploads, done properly.
Application security discussions tend to drift toward the interesting and rare. Meanwhile the breaches that actually happen come from the same handful of ordinary mistakes, repeated.
Prepared statements, without exception
String concatenation into SQL is still common, usually in the one query someone wrote in a hurry. The rule has to be absolute: every value that reaches a query is a bound parameter, no exceptions for internal admin screens or "just an integer".
Table and column names cannot be parameterised, so they must be whitelisted against a known list rather than passed through from input.
CSRF tokens on every state-changing request
A token in the session, a matching hidden field in the form, and a comparison with hash_equals() — not ==, which is vulnerable to timing analysis and to PHP's type juggling.
The common gap is AJAX. If your JavaScript posts without the token, either include it in a header or the endpoint is unprotected. Rotate the token after any successful sensitive action.
Sessions need more than session_start()
- Set
HttpOnly,SecureandSameSiteon the session cookie. - Regenerate the session ID on privilege change — login is the obvious one.
- Enforce an idle timeout, and rotate the ID periodically during long sessions.
- Bind the session to a fingerprint of the user agent and network, and drop it if that changes.
File uploads: never trust the extension
The extension is user-supplied. So is the MIME type in the request. Check the actual file contents with finfo, verify images parse with getimagesize(), generate a new filename rather than using the supplied one, and store uploads in a directory where script execution is blocked at the server level.
SVG deserves particular care: it is XML and can carry script. Either strip it or reject it.
Password storage
password_hash() with a sensible cost, password_verify() to check, and password_needs_rehash() to upgrade silently when you raise the cost. Nothing else. No custom salting schemes, no SHA-256 with a pepper someone read about.
Output escaping is contextual
htmlspecialchars() with ENT_QUOTES handles HTML body and attribute contexts. It does not make a value safe inside a JavaScript block, a URL parameter or a CSS declaration. Those need their own encoding, and the safest answer is usually to avoid putting user data in those contexts at all.
Rate limiting is a security control
Login, password reset, contact forms and search endpoints all need limits. Without them, a brute-force attempt is only constrained by bandwidth. Track by IP and by account, and prefer lockout with a clear message over silent failure.
None of this is difficult
That is rather the point. These are all one-time implementations that then apply automatically across the application. The cost of getting them right at the start is a few days. The cost of getting them wrong is considerably higher.