Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Защита от CSRF

CSRF — подделка запроса с чужого сайта. Посетитель вошёл в ваш блог, затем открыл чужую страницу, а она незаметно отправила форму на ваш адрес POST /posts. Браузер приложит cookie сессии, и приложение примет запрос за настоящий. Защита — секретный токен: случайная строка, которую знает только ваша форма.

Установка

composer require slim/csrf

Пакет даёт посредник Slim\Csrf\Guard. Он создаёт токен, хранит его в сессии и проверяет в каждом запросе POST, PUT, PATCH и DELETE.

Описание в контейнере

// 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),
),

Посреднику нужна фабрика ответов — ею он создаёт ответ с ошибкой, когда токен не совпал.

Подключение

Защита нужна страницам сайта, но не API из следующей главы: там нет сессии и cookie. Поэтому посредник привязывают не ко всему приложению, а к группе, в которую заключены все маршруты сайта:

// 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);
    // ... остальные маршруты сайта
})->add(Guard::class);

Посредники маршрутов выполняются после посредников приложения, поэтому к этому моменту сессия уже запущена, а тело запроса разобрано — и то и другое нужно для проверки.

Токен в действии

Посредник кладёт пару «имя и значение токена» в атрибуты запроса. Действие, которое показывает форму, передаёт их в шаблон:

// src/Action/NewPostAction.php
return $this->twig->render($response, 'posts/new.twig', [
    'csrf_name' => $request->getAttribute('csrf_name'),
    'csrf_value' => $request->getAttribute('csrf_value'),
]);

Поля в форме

Внутри тега form добавьте два скрытых поля:

{# templates/posts/new.twig #}
<input type="hidden" name="csrf_name" value="{{ csrf_name }}">
<input type="hidden" name="csrf_value" value="{{ csrf_value }}">

Откройте /posts/new и посмотрите исходный код страницы: в полях стоят случайные строки. Отправка формы работает как раньше — посредник сверяет поля с сессией и пропускает запрос.

Те же два поля нужны во всех формах сайта, включая форму входа. Чтобы не повторять разметку, вынесите их в файл templates/_csrf.twig и подключайте строкой {% include '_csrf.twig' %}.

Проверка защиты

Отправьте запрос без токена:

curl -i -X POST -d "title=Проверка" http://localhost:8080/posts

В ответ придёт код 400 — запрос отклонён ещё до действия.

Форма после ошибки

По умолчанию токен одноразовый: на каждый запрос посредник выдаёт новую пару. Поэтому, когда CreatePostAction выводит форму повторно с ошибками проверки, в шаблон тоже нужно передать csrf_name и csrf_value из текущего запроса — иначе вторая отправка будет отклонена.

Частая ошибка

Guard хранит токены в сессии, и создать его до запуска сессии нельзя — он бросит исключение. Передавайте в add() имя класса Guard::class, а не готовый объект: тогда Slim создаст посредник только в момент запроса, когда SessionMiddleware уже отработал.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.