Security in Depth
XSS (Intermediate Chapter 8) and password hashing (Intermediate Chapter 5) have already been covered properly. This chapter revisits both briefly as a security-focused recap, then introduces CSRF — the one major web vulnerability category not yet addressed anywhere in this series.
CSRF — Cross-Site Request Forgery
Imagine a logged-in visitor (with a valid session, Intermediate Chapter 5) visits a malicious page that auto-submits a hidden form to yoursite.com/transfer-money. Because the visitor's browser automatically attaches their session cookie to any request to that domain, the malicious request looks completely legitimate to the server — even though the visitor never intended to submit it.
The Fix — CSRF Tokens
The token is embedded in the legitimate form, and re-checked on submission. The malicious page from the diagram above has no way to know or guess this token — it isn't part of the session cookie the browser sends automatically, so the forged request fails the check.
hash_equals() performs a "timing-safe" comparison — a regular === string comparison can, in theory, take very slightly different amounts of time depending on how many leading characters match, which an attacker could measure over many attempts to slowly guess a secret value. hash_equals() is specifically designed to take the same amount of time regardless, which matters for comparing security tokens (though, with a 32-byte random token, brute-forcing is already practically infeasible regardless).
XSS Prevention — A Quick Recap
Nothing new here beyond Intermediate Chapter 8's rule — included as a reminder that CSRF and XSS are entirely separate vulnerabilities requiring entirely separate fixes; defending against one does nothing for the other.
Password Hashing — A Quick Recap, Plus One Addition
password_needs_rehash() detects when a stored hash was created with weaker settings than PHP's current default — checked at login time (when the plain password is briefly available anyway), letting old hashes be quietly upgraded over time as users log in, without forcing a mass password reset.
A Combined Security Checklist
- Escape every piece of output with htmlspecialchars() — prevents XSS
- Use prepared statements for every database query — prevents SQL injection
- Include and verify a CSRF token on every state-changing form — prevents CSRF
- Hash passwords with password_hash(), verify with password_verify() — never store plain text
- Validate and sanitise file uploads, never trust the claimed MIME type — prevents malicious uploads
- Always call exit immediately after a header() redirect — prevents protected content leaking
Coding Challenges
Write a complete CSRF-protected form flow: a page generating and embedding a token, and a processing script that rejects the request with a 403 status if the token is missing or doesn't match, using hash_equals(). Test it conceptually by describing what would happen if an attacker's page tried to submit the same form action directly, without the token.
📄 View solutionWrite a function verifyAndUpgradePassword(string $password, string &$storedHash): bool that returns false immediately if the password doesn't verify, otherwise checks password_needs_rehash() and updates $storedHash (passed by reference) with a freshly generated hash if needed, then returns true. Explain in a comment why $storedHash needs to be passed by reference here.
Review this code and list every security issue you can find, then write a corrected version: session_start(); $name = $_POST['name']; echo "<form method='post'><input name='name' value='$name'><input type='submit'></form>"; $stmt = $pdo->query("SELECT * FROM users WHERE name = '$name'");
Chapter 5 Quick Reference
- CSRF — a forged request riding on a visitor's valid session, from a different malicious site
- CSRF token — a random, unguessable value embedded in forms and re-checked on submission
- hash_equals() — timing-safe comparison, used for comparing security tokens
- htmlspecialchars() — XSS prevention, on every output (Intermediate Chapter 8 recap)
- password_hash() / password_verify() — never store plain-text passwords (Intermediate Chapter 5 recap)
- password_needs_rehash() — detects outdated hash settings, allows quiet upgrades at login time
- CSRF, XSS, and SQL injection are three SEPARATE vulnerabilities — each needs its own specific defence
- Next chapter: performance — opcache, profiling, caching strategies