Cross-site request forgery (CSRF) abuses the fact that browsers attach your cookies to every request, including one a different site triggers. If a logged-in user visits an attacker's page, a hidden form there can POST to your application with the session cookie riding along, and the action goes through as if the user asked. The defense is a secret the attacker cannot know: a synchronizer token in the session.
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32));
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($safe && !hash_equals($_SESSION['csrf'], (string) ($_POST['csrf'] ?? ''))) {
http_response_code(403); exit("403 CSRF token missing or wrong\n");
}
$_SESSION['email'] = $_POST['email'] ?? $_SESSION['email']; // the sensitive action
}With a saved cookie jar: the victim loads the page, the attacker forges a POST with only the cookie against each endpoint, then a real POST carries the session token:
current email: owner@example.com token: 7e0bdfd72872613a58fdb561b652c0444f4cbd9b8dd44a2683d736 700d3ba0cd email changed to attacker@evil.test 403 CSRF token missing or wrong email changed to new@example.com
Carrying only the session cookie, the forged POST changed the account email on the vulnerable endpoint. The hardened endpoint rejected it with 403 for want of a token, and accepted it only with the real session token. Compare tokens with hash_equals(), not == (Subsection 4.17.11), render the token into every form as a hidden field, and send it in a header for fetch(). The complementary defense is SameSite (Subsection 4.15.3): set Lax or Strict and the cookie is not sent on a cross-site POST. Use both.