A browser sends your cookies with any request to your domain, whoever caused the request. So a page on another site can contain a form that posts to yours, and if your visitor is logged in, it arrives authenticated - and looks completely legitimate in your logs.
What it can do
- Change an email address, then request a password reset to the new one.
- Place an order, transfer something, delete something.
- Anything your site does with a POST and a session.
The token
A random value stored in the session and repeated in the form. A page on another site cannot read your session and therefore cannot know the value.
// when rendering the form
$_SESSION["csrf"] = $_SESSION["csrf"] ?? bin2hex(random_bytes(32));
?>
<input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
// when handling it
if (!hash_equals($_SESSION["csrf"] ?? "", $_POST["csrf"] ?? "")) {
http_response_code(400); exit;
}
Compare with hash_equals, not with ==. A plain comparison stops at the first differing character, and the time it takes leaks how much of the token was right.
And the cookie attribute
session.cookie_samesite = Lax
SameSite=Lax tells the browser not to send the cookie on a cross-site POST at all, which stops this class before your code sees it. It is a second layer, not a replacement: it protects only where the browser is recent and behaving.
GET must not change anything
A link that deletes something is a CSRF waiting to happen - and worse, a crawler or a link preview will trigger it on its own. Anything that changes state is a POST with a token.
A framework has this built in and switched on. If you have turned it off for one endpoint to make something work, that endpoint is the one to look at.