CSRF protection
CSRF is forging a request from another site. A visitor logs in to your blog, then opens someone else's page, and that page silently submits a form to your POST /posts address. The browser attaches the session cookie, and the application takes the request for a genuine one. The protection is a secret token: a random string known only to your form.
Installation
composer require slim/csrfThe package provides the Slim\Csrf\Guard middleware. It creates a token, keeps it in the session and checks it on every POST, PUT, PATCH and DELETE request.
The container definition
// config/container.php
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Slim\Csrf\Guard;
Guard::class => fn (ContainerInterface $c) => new Guard(
$c->get(ResponseFactoryInterface::class),
),The middleware needs the response factory — it uses it to create the error response when the token does not match.
Attaching it
The site pages need the protection, but the API from the next chapter does not: it has no session and no cookie. So the middleware is attached not to the whole application but to a group that encloses all site routes:
// config/routes.php
use Slim\Csrf\Guard;
$app->group('', function (RouteCollectorProxy $site) {
$site->get('/', HomeAction::class)->setName('home');
$site->get('/login', LoginFormAction::class)->setName('login');
$site->post('/login', LoginAction::class);
// ... the other site routes
})->add(Guard::class);Route middleware runs after application middleware, so by this point the session is started and the request body is parsed — the check needs both.
The token in the action
The middleware puts a "token name and value" pair into the request attributes. The action that shows the form passes them to the template:
// src/Action/NewPostAction.php
return $this->twig->render($response, 'posts/new.twig', [
'csrf_name' => $request->getAttribute('csrf_name'),
'csrf_value' => $request->getAttribute('csrf_value'),
]);The form fields
Inside the form tag add two hidden fields:
{# templates/posts/new.twig #}
<input type="hidden" name="csrf_name" value="{{ csrf_name }}">
<input type="hidden" name="csrf_value" value="{{ csrf_value }}">Open /posts/new and view the page source: the fields hold random strings. Submitting the form works as before — the middleware compares the fields with the session and lets the request through.
The same two fields are needed in every form of the site, the login form included. To avoid repeating the markup, move them into templates/_csrf.twig and include it with the line {% include '_csrf.twig' %}.
Checking the protection
Send a request without a token:
curl -i -X POST -d "title=Check" http://localhost:8080/postsThe answer is code 400 — the request is rejected before it reaches the action.
The form after an error
By default a token is single-use: the middleware issues a new pair for every request. So when CreatePostAction renders the form again with validation errors, the template must also receive csrf_name and csrf_value from the current request — otherwise the second submission is rejected.
Common mistake
Guard keeps tokens in the session and cannot be created before the session starts — it throws an exception. Pass the class name Guard::class to add(), not a ready object: then Slim creates the middleware only at request time, after SessionMiddleware has already run.