CSRF Protection
- Overview
- Configuration
- Protected Verbs
- Adding the Token to a Form
- Sending the Token with AJAX Requests
- Token Lifetime
- Exempting an Endpoint
- Validating the Token Manually
Overview
Sukarix provides built-in CSRF (Cross-Site Request Forgery) protection mechanisms to ensure the security of your forms and requests. By enabling CSRF protection, you can safeguard your application from unauthorized actions performed by malicious users.
Protection is enforced automatically: every action extending Sukarix\Actions\Action validates the token in
beforeroute() before your handler runs, so you never have to call the validation yourself.
Configuration
CSRF protection is configured in default.ini or the environment-specific config-<environment>.ini. The following default settings enable CSRF protection and set
the token expiry time:
[SECURITY]
csrf.enabled = true
csrf.expiry = 3600
- csrf.enabled: Enables or disables CSRF protection.
- csrf.expiry: Sets the expiry time for the CSRF token in seconds (default is 3600 seconds or 1 hour).
Protected Verbs
The token is required on every state changing request, that is POST, PUT, DELETE and PATCH. GET, HEAD and
OPTIONS requests are never validated.
When validation fails, the request is not passed to your action: it is rerouted to the current path, and
form_errors.csrf_token is set in the session so the form can report the problem. A csrf_bounce session message is
also set, with a message suitable for display directly to the user (Your form session expired. Please try again.).
The rejection is also logged at critical level with the route alias, the verb and the client IP:
Invalid CSRF token {"alias":"login","verb":"POST","ip":"203.0.113.7"}
Tip
A form that silently reloads instead of submitting
Because a rejected request is rerouted rather than answered with an error page, a missing token looks like a form that reloads and does nothing. If a form appears inert, grep your logs for
Invalid CSRF tokenbefore looking anywhere else.
Adding the Token to a Form
To include a hidden CSRF token input in your forms, use the <csrf/> template directive. This directive generates a
hidden input field named csrf_token, ensuring that each form submission includes a valid token.
Example Usage
<form method="post" action="{{ @ALIASES.login }}">
<csrf/>
<!-- other form fields -->
<input type="submit" value="Submit">
</form>
Add the directive to every form that is not a GET form. A form without it cannot be submitted while CSRF
protection is enabled.
Sending the Token with AJAX Requests
A hidden field only covers form encoded submissions. Two common cases cannot use one:
- Requests sending a JSON body, because PHP only populates
$_POSTfor form encoded bodies. PUT,DELETEandPATCHrequests, because Fat-Free only mapsGET,POSTandCOOKIEinto the hive, so a token sent as aPUTparameter is never readable.
Those requests must send the token in the X-Csrf-Token header instead. Expose the token in your layout:
<meta name="csrf-token" content="{{ @SESSION.csrf_token }}"/>
Then attach it to every state changing request. With jQuery, a single global hook covers the whole application:
$(document).ajaxSend(function (event, jqXHR, settings) {
if (/^(GET|HEAD|OPTIONS)$/i.test(settings.type)) {
return;
}
// Never disclose the token to another origin
if (/^(https?:)?\/\//i.test(settings.url) && settings.url.indexOf(window.location.origin) !== 0) {
return;
}
let token = $('meta[name="csrf-token"]').attr('content');
if (token) {
jqXHR.setRequestHeader('X-CSRF-Token', token);
}
});
Header names are case insensitive over HTTP, and Fat-Free normalises them, so the framework always reads the value from
HEADERS.X-Csrf-Token. Both names are available as constants:
Sukarix\Core\Session::CSRF_HEADER; // 'X-Csrf-Token'
Sukarix\Core\Session::CSRF_FIELD; // 'csrf_token'
The header takes precedence over the request parameter, so a request may safely send both.
Token Lifetime
Each session holds a single token, which stays valid until it expires. It is not single use: the same token validates any number of requests within its expiry window. This matters in two everyday cases:
- A page holding several forms — all of them carry the same valid token.
- A page performing successive writes without reloading, as an AJAX driven table does.
Within a single request the token is stable, so rendering <csrf/> more than once always yields the same value. Once the
token expires, the next request issues a new one.
Note
CSRF Token Expiry
The CSRF token has an expiry time configured in
default.inior the environment-specificconfig-<environment>.ini. If the token has expired, it will be considered invalid. Ensure your application handles token expiry by prompting the user to refresh the form or session.
Exempting an Endpoint
Endpoints called by external systems — webhooks, telephony gateways, payment callbacks — hold no session and cannot
carry a token. Set the $csrfExempt property on those actions:
class Callback extends BaseAction
{
protected bool $csrfExempt = true;
public function execute($f3, $params): void
{
// ...
}
}
Warning
An exempt endpoint must authenticate its caller
Exempting an action removes its only CSRF defence. Such an endpoint has to identify the caller by its own means, for instance a shared secret, a request signature, or matching the request against a known host. Never exempt an action that acts on the current user’s session.
Validating the Token Manually
Automatic enforcement is usually enough. When you need the outcome inside a handler, use the isCsrfValid() method from
the session. This method reports whether the token submitted with the request matched the token stored in the user’s
session.
Example Usage
if ($this->session->isCsrfValid()) {
// Token is valid, proceed with form processing
} else {
// Token is invalid, handle the error
}
This code snippet shows how to check the CSRF token when processing a form submission. If the token is valid, you can proceed with the form processing. If the token is invalid, handle the error appropriately.