-
Notifications
You must be signed in to change notification settings - Fork 0
Recipe Csrf Token
A CSRF token is a per-session secret that the server issues and the client echoes back on every state-changing request. It defeats the class of attacks where a third-party site causes the user's browser to issue an authenticated request without their knowledge.
This recipe uses a dedicated Segment to hold the token,
rotate it on login, and validate it on POST/PUT/DELETE.
For every authenticated request:
- If no CSRF token exists in the session segment, generate one.
- If the request is state-changing (POST/PUT/PATCH/DELETE), require a matching token in the request body.
- On a successful login (or any privilege escalation), rotate the token so a token captured pre-login cannot be replayed.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use InitPHP\Auth\Segment;
session_start();
$csrf = Segment::session('csrf');
if (!$csrf->has('token')) {
$csrf->set('token', bin2hex(random_bytes(32)));
}
$csrfToken = $csrf->get('token');?>
<form method="post" action="/comments">
<input type="hidden" name="_csrf" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
<textarea name="body"></textarea>
<button>Post</button>
</form>
<?php$method = $_SERVER['REQUEST_METHOD'];
if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
$submitted = $_POST['_csrf'] ?? '';
$expected = $csrf->get('token', '');
if (!hash_equals($expected, $submitted)) {
http_response_code(419);
exit('CSRF validation failed.');
}
}Note the use of hash_equals() — === is a timing oracle on the
token and must be avoided.
The right time to rotate is immediately after a successful authentication and any privilege escalation. A token issued before login should not survive into the authenticated session.
// inside your login handler, after password_verify() succeeds:
$auth->set('user_id', $user['id']);
session_regenerate_id(true);
$csrf->set('token', bin2hex(random_bytes(32)));session_regenerate_id(true) is the standard defence against session
fixation. Pair it with the CSRF rotation so a pre-login fixture token
is also invalidated.
| Step | Where | What it does |
|---|---|---|
| Bootstrap | Front controller | Ensures every request has a CSRF token to render into forms. |
| Embed | Form template | Sends the token to the client in the rendered HTML. |
| Validate | Front controller (state-changing branch) | Constant-time compare between the submitted and stored tokens. |
| Rotate | Login / privilege-change handler | Replaces the token so pre-login captures cannot be replayed. |
The csrf segment is intentionally separate from the auth segment.
That way:
- A logged-out user still has CSRF protection for state-changing endpoints (e.g. account-creation flow).
- A logout that wipes the auth segment leaves the CSRF token in place, so the next form submission still validates.
- The CSRF token can be rotated independently of the auth state.
The pattern above uses a hidden form field, which works for any
server-rendered HTML. APIs and SPAs usually prefer a custom header
(X-CSRF-Token) that they read once from a <meta> tag and attach to
every XHR / fetch. Either pattern works — the segment store stays the
same; only the validation step changes:
$submitted = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
$expected = $csrf->get('token', '');
if (!hash_equals($expected, $submitted)) {
http_response_code(419);
exit('CSRF validation failed.');
}If you cannot keep a session at all (a truly stateless API), use the
double-submit cookie pattern
instead. Issue the CSRF token as a Secure; SameSite=Strict cookie
without the HttpOnly flag, and require the client to echo it
back in a header on every state-changing request. The server then
compares the cookie value against the header value with
hash_equals().
You can model the cookie side of that pattern with CookieAdapter,
but it is usually overkill — a plain setcookie('csrf', $token, [...])
is enough because the server never reads or signs the value; it only
checks that the header matches the cookie.
- Token TTL. A session-bound token expires when the session does. If you set short session lifetimes, generate a fresh token on every request rather than per session.
- Token uniqueness per form. Some libraries issue a fresh token per form, which lets you scope CSRF protection more tightly. For most apps the single-per-session token above is enough.
-
Header on AJAX. Browsers do not include custom headers on
cross-origin requests by default, which is what makes the
X-CSRF-Tokenheader pattern work. - Logging failures. A CSRF validation failure on an authenticated endpoint is worth logging as a security event — repeated failures from one IP are a strong signal.
-
Comparing with
===or==. Usehash_equals(). Anything else is a timing oracle. - Skipping the rotation on login. Without rotation, a CSRF token captured pre-login lets an attacker forge requests once the user authenticates.
- Storing the token in a cookie that the server reads. That is not CSRF protection — the attacker's malicious request would also carry the cookie. Either keep the token in the session (this recipe) or use the double-submit pattern where the header is what proves the request originated from your site.
- Reusing the same token across browser tabs without rotation. Tab A signs in, rotates the token, and tab B is suddenly invalid for the next POST. Accept this and refresh the token at the top of every authenticated page render; or use per-form tokens.
- Recipe → Multi-Segment — auth + cart + csrf together.
- Security — broader operational concerns.
- Session Adapter — the backing store this recipe uses.
initphp/auth · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Core Types
Adapters
Reference
Recipes
Migration & Help