Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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/csrf

The 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/posts

The 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.

Thanks for reading!

Donate to the site — we work hard for you.