Spiral provides built-in support for CSRF (Cross-Site Request Forgery) protection, making it easy for developers to implement this important security measure in their web applications and ensure that any actions taken on the website are intended by the user and not the result of a malicious attack.
Spiral uses cookies to store the CSRF token and does not rely on server-side sessions. This approach is considered to be more efficient and simple, as it reduces the need for server-side storage and allows for faster performance.
Let's imagine your application has a page to change the user's password and user can send a POST
request to this page
with a new password. If application does not properly validate the authenticity of the user, an attacker may be able to
change the of user's password by tricking application into thinking the request is coming from the actual user. This can
be done via a malicious page.
Here is an example of a malicious page:
<form action="https://your-application.com/user/password" method="POST">
<input name="password" type="password" value="secret">
</form>
<script>
document.forms[0].submit();
</script>
Without CSRF protection, the user's password will be changed when a user visits a page with malicious code.
To prevent this vulnerability, it's important to implement proper CSRF protection in your application. One way to do
this is by inspecting every incoming POST
, PUT
, PATCH
, or DELETE
request for a CSRF token value that the
malicious website is unable to access. The token often referred to as a CSRF token, can be generated on the server and
included as a hidden field in forms.
<form action="https://your-application.com/user/password" method="POST">
<input type="hidden" name="csrf-token" value="{csrfToken}"/>
<input name="password" type="password">
// ...
<button type="submit">Change password</button>
</form>
When the user submits a form, the server can then compare the value of the CSRF token included in the request to the value stored in user cookie. If the values do not match, the server can reject the request and prevent the malicious action from being performed.
The default spiral/app
includes CSRF protection middleware.
To install it in alternative application:
Add Spiral\Bootloader\Http\CsrfBootloader
to the list of bootloaders:
public function defineBootloaders(): array
{
return [
// ...
\Spiral\Bootloader\Http\CsrfBootloader::class,
// ...
];
}
Read more about bootloaders in the Framework — Bootloaders section.
After the bootloader is added you need to enable Spiral\Csrf\Middleware\CsrfMiddleware
middleware to issue a unique
token for every user.
To enable the middleware, add it to middleware group you want to protect:
namespace App\Application\Bootloader;
use Spiral\Cookies\Middleware\CookiesMiddleware;
use Spiral\Csrf\Middleware\CsrfMiddleware;
final class RoutesBootloader extends BaseRoutesBootloader
{
protected function middlewareGroups(): array
{
return [
'web' => [
CookiesMiddleware::class,
CsrfMiddleware::class,
// ...
],
// ...
];
}
// ...
}
See more
Read more about global middleware in the HTTP — Middleware section.
After the middleware is added, you may configure some options via app/config/csrf.php
.
Here is the default configuration:
return [
'cookie' => 'csrf-token',
'length' => 16,
'lifetime' => 86400,
'secure' => true,
'sameSite' => null,
];
Warning
In you changecookie
option, you must also add it to the whitelist cookie list. Read more how to do it in the HTTP — Cookies section.
The component provides two middlewares which activate protection on your routes.
To protect all the requests except for GET
, HEAD
, OPTIONS
, use Spiral\Csrf\Middleware\CsrfFirewall
:
use Spiral\Csrf\Middleware\CsrfFirewall;
'web' => [
CookiesMiddleware::class,
CsrfMiddleware::class,
CsrfFirewall::class,
// ...
],
Note
To protect against all the HTTP verbs, useSpiral\Csrf\Middleware\StrictCsrfFirewall
.
Once the protection firewall is activated, you must sign desired forms with the token available via PSR-7
attribute csrfToken
.
Note
csrfToken
attribute is generated bySpiral\Csrf\Middleware\CsrfMiddleware
middleware on every request.
To receive this token from the request in the controller or view use getAttribute
method:
public function index(ServerRequestInterface $request): void
{
$csrfToken = $request->getAttribute('csrfToken');
}
Every POST
/PUT
/DELETE
request from the user must include this token as POST parameter csrf-token
or
header X-CSRF-Token
. Users will receive 412 Bad CSRF Token
if a token is missing or not set.
use Psr\Http\Message\ServerRequestInterface;
// ...
public function changePasswordForm(ServerRequestInterface $request): string
{
$form = <<<FORM
<form action="https://your-application.com/user/password" method="POST">
<input type="hidden" name="csrf-token" value="{csrfToken}"/>
<input name="password" type="password">
// ...
<button type="submit">Change password</button>
</form>
FORM;
return \str_replace(
'{csrfToken}',
$request->getAttribute('csrfToken'),
$form
);
}
You can also use view global variables to define csrf-token
globally for all view templates.
Here is an example how to do it via middleware:
use Psr\Http\Server\MiddlewareInterface;
use Spiral\Views\GlobalVariablesInterface ;
class ViewCsrfTokenMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly GlobalVariablesInterface $globalVariables
) {}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$this->globalVariables->set('csrfToken', $request->getAttribute('csrfToken'));
return $handler->handle($request)->withAddedHeader('My-Header', 'my-value');
}
}
See more
Read more about global variables in the Views — Basics section.
Don't forget to add the middleware to the middleware list.
'web' => [
CookiesMiddleware::class,
CsrfMiddleware::class,
ViewCsrfTokenMiddleware::class,
CsrfFirewall::class,
// ...
],
After that, you can use the csrfToken
variable in your views:
<form action="https://your-application.com/user/password" method="POST">
<input type="hidden" name="csrf-token" value="{csrfToken}"/>
<input name="password" type="password">
// ...
<button type="submit">Change password</button>
</form>