PHP application security
Every vulnerability in this topic is the same mistake wearing different costumes: data controlled by a stranger reaches a place where it is read as instructions. In SQL injection the input becomes query syntax. In XSS it becomes markup in someone else's browser. In mass assignment it becomes a column name. And the defence, in every case, has the same shape: separate data from code structurally, rather than trying to scrub dangerous characters out of the data.
Which is why the wording matters so much in an interview. "A prepared statement escapes the input" is the wrong mechanism, and it gives a candidate away instantly. The right one: the server parses and plans the statement before it receives a single value, so a parameter physically cannot become SQL. The same holds elsewhere: "we salt our passwords" is the wrong mechanism (a salt breaks rainbow tables but does not slow brute force down), while the right one is that the hash must be deliberately slow. And likewise: htmlspecialchars() covers the HTML text context and does not cover a JavaScript, URL or unquoted-attribute context, because escaping is context-dependent. The layer-by-layer breakdown is below.
Topic map
- SQL injection — why the statement is parsed before the parameters are bound, what
PDO::ATTR_EMULATE_PREPARESbreaks, and whyPDO::quote()is no substitute for binding. - Input validation — validation versus output encoding, and the allowlist for the one thing that cannot be bound: a column name in
ORDER BY. - XSS — which output context
htmlspecialchars()actually covers, and why it is useless inside<script>, in a URL, and in an unquoted attribute. - CSRF — why the browser attaches your cookie by itself, what the token proves, and why
SameSiteis defence in depth rather than a replacement. - Session security — the session id as a bearer token, the cookie flags, session fixation, and
session_regenerate_id(true). - Password hashing — the deliberately slow hash,
password_hash()andpassword_verify()against the fast MD5 and SHA-1, and why "just add a salt" is the wrong answer. - Secrets management — why a committed secret lives in history forever, and why a leaked key is rotated rather than deleted.
- Rate limiting — the counter in a shared store, keying by identity, the window,
429withRetry-After, and the fail-open versus fail-closed choice.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Saying a prepared statement "escapes the input" | Wrong mechanism: the statement is parsed before binding, so a parameter cannot become SQL |
Leaving PDO::ATTR_EMULATE_PREPARES on (the MySQL driver's default) | There is no server-side parse before binding at all: PDO assembles the SQL client-side |
Treating PDO::quote() or addslashes() as equivalent protection | That is escaping, at every call site — miss it once and the injection is open |
Trying to bind a column name or ASC/DESC in ORDER BY | A placeholder is a slot for a value: you would sort by a constant string. This needs an allowlist |
| Allowlisting the column but interpolating the sort direction from the request | ORDER BY title {$_GET['dir']} is the same injection, just moved |
| Treating validation as a defence against XSS | Validation narrows the input but does not make it printable — XSS is stopped by encoding |
| Encoding on input and storing the encoded value | The value is now corrupt for every other context: JSON, email, PDF, search |
Believing htmlspecialchars() is universal | It covers HTML text and (with ENT_QUOTES) a quoted attribute — and does not cover <script>, javascript:, or an unquoted attribute |
Thinking htmlspecialchars() strips tags | It encodes characters and removes nothing |
| Believing CSRF means the attacker stole the session id | No: the cookie is sent automatically — the attacker never sees it, they merely make the browser attach it |
Believing SameSite makes the CSRF token unnecessary | It is defence in depth, not a replacement: old browsers, subdomains and SameSite=None remain |
Relying on the Referer header | It can be absent, or stripped by a proxy or a policy — you cannot base a decision on it |
| Thinking a salt fixes MD5 | A salt breaks rainbow tables but does not slow brute force: the point is a deliberately slow hash |
| Storing the salt or the cost in a separate column | The password_hash() string already carries the algorithm, the cost and the salt inside it |
Comparing hashes with === | You need password_verify(): it reads the parameters back out of the hash and compares in constant time |
| Regenerating the session id on logout but not on login | The privilege change happens at login — which is exactly where session fixation lives |
| Believing a committed secret is safe because the repository is private | It is in every clone, fork and CI log; deleting the line does not remove it from history |
| Keeping the rate-limit counter in worker memory | Every worker hands out the full limit: the real limit is multiplied by the worker count |
Interview relevance
Security is where candidates are separated by the precision with which they state the mechanism. Everyone knows the words "prepared statements", "salt", "token", "escaping". The difference is what stands behind them. A candidate who says "PDO escapes the input" cannot explain why ORDER BY is unbindable — whereas one who says "the statement is parsed before the values arrive" derives that consequence themselves, from the mechanism.
Typical checks:
- What exactly makes a prepared statement immune — and what emulation mode changes.
- What can be bound and what must go through an allowlist.
- How validation differs from encoding, and which of the two stops XSS.
- Which context
htmlspecialchars()covers and which three it does not. - Why CSRF works at all and what the token proves.
- Why
password_hash()rather than "MD5 with a salt", and what is already inside the hash string. - What session fixation is and which single call at login defeats it.
- Where the rate-limit counter lives and why not in PHP's memory.
Common wrong answer: "We escape everything." Escaping answers the question "how do I neutralise these characters", but the question is a different one: "how do I make sure the data never lands in a code position at all". A prepared statement answers the second — structurally. The second classic failure is "htmlspecialchars protects against XSS": it protects in one context, and a string that is safe as HTML text is still executable code inside a <script> block.