Как реализовать CSRF-защиту

Дата обновления перевода 2025-02-15

Как реализовать CSRF-защиту

CSRF - или Межсайтовая подделка запроса - это тип атаки, при которой злоумышленник обманом заставляет пользователя выполнять действия в веб-приложении без его ведома или согласия.

Атака основана на доверии веб-приложения к браузеру пользователя (например, к куки сессии). Вот реальный пример CSRF-атаки: злоумышленник
может создать следующий веб-сайт:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
    <body>
        <form action="https://example.com/settings/update-email" method="POST">
            <input type="hidden" name="email" value="malicious-actor-address@some-domain.com"/>
        </form>
        <script>
            document.forms[0].submit();
        </script>

        <!-- какое-то содержание для отвлечения внимания пользователя -->
    </body>
</html>

Если вы посетите этот сайт (например, перейдя по ссылке в электронной почте или в социальной
сети) и уже были авторизованы на сайте https://example.com, злоумышленник может изменить адрес электронной почты, связанный с вашей учетной записью
(фактически завладев вашей учетной записью) без вашего ведома.

Эффективным способом предотвращения CSRF-атак является использование анти-CSRF-токенов. Это уникальные маркеры, добавляемые в формы в качестве скрытых полей. Легитимный сервер проверяет их, чтобы убедиться, что запрос поступил от ожидаемого источника, а не от какого-то другого вредоносного сайта.

Установка

Symfony предоставляет все необходимые функции для генерирования и валидации анти-CSRF токенов. Прежде чем использовать их, установите этот пакет в своем проекте:

1
$ composer require symfony/security-csrf

Затем включите/отключите защиту CSRF с помощью опции csrf_protection. (см. справочник конфигурации CSRF для получения дополнительной информации):

1
2
3
4
# config/packages/framework.yaml
framework:
    # ...
    csrf_protection: ~

Токены, используемые для CSRF-защиты, должны быть разными для каждого пользователя, и они хранятся в сессии. Поэтому сессия начинается автоматически, как только вы отображаете форму с CSRF-защитой.

Более того, это означает, что вы не можете полностью кешировать страницы, которые имеют формы с защитой от CSRF. Как вариант, вы можете:

  • Встроить форму внутрь некешируемого фрагмента ESI и кешировать остаток содержания страницы;
  • Кешировать всю страницу и загрузить форму через некешируемый запрос AJAX;
  • Кешировать всю страницу и использовать hinclude.js для загрузки CSRF-токена с некешируемым запросом AJAX, и заменить значение поля формы им.

CSRF-защита в формах Symfony

Формы Symfony включают в себя токены CSRF по умолчанию, и Symfony проверяет их автоматически, так что вам не нужно ничего делать, чтобы быть защищёнными от CSRF атак.

По умолчанию Symfony добавляет CSRF-токен в скрытое поле под названием _token, но это можно настроить (1) глобально для всех форм и (2) отдельно для каждой формы. Глобально вы можете сконфигурировать это в опции framework.form:

1
2
3
4
5
6
7
# config/packages/framework.yaml
framework:
    # ...
    form:
        csrf_protection:
            enabled: true
            field_name: 'custom_token_name'

Для каждой отдельной формы вы можете сконфигурировать защиту от CSRF в методе setDefaults(). каждой формы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Form/TaskType.php
namespace App\Form;

// ...
use App\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;

class TaskType extends AbstractType
{
    // ...

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class'      => Task::class,
            // включить/отключить защиту от CSRF для этой формы
            'csrf_protection' => true,
            // имя скрытого HTML-поля, в котором хранится токен
            'csrf_field_name' => '_token',
            // произвольная строка, используемая для генерирования значения токена
            // использование разных строк для каждой формы повышает ее безопасность
            'csrf_token_id'   => 'task_item',
        ]);
    }

    // ...
}

Вы также можете настроить отображение поля формы CSRF, создав пользовательскую тему формы и используя csrf_token в качестве префикса поля (например, определить {% block csrf_token_widget %} ... {% endblock %}, чтобы настроить все содержание поля формы).

Защита от CSRF в форме входа и действии выхода из системы

Прочтите следующее:

  • Защита от CSRF в формах входа ;
  • Защита от CSRF для действия выхода из системы .

Генерирование и проверка CSRF-токенов вручную

Хотя Формы Symfony предоставляют автоматическую CSRF-защиту по умолчанию, вам может понадобиться сгенерировать и проверить CSRF-токены вручную, например, при использовании обычных HTML-форм, не управляемыъ компонентом Symfony Формы.

Рассмотрите HTML-форму, созданную для позволения удаления объектов. Для начала, используйте функцию Twig csrf_token() , чтобы сгенерировать CSRF-токен в шаблоне и сохранить его в скрытом поле формы:

1
2
3
4
5
6
<form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post">
    {# аргумент csrf_token() это произвольная строка. используемая для генерирования токена #}
    <input type="hidden" name="token" value="{{ csrf_token('delete-item') }}">

    <button type="submit">Delete item</button>
</form>

Затем, получите значение CSRF-токена в действии контроллера и используйте метод isCsrfTokenValid() чтобы проверить его валидность:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ...

public function delete(Request $request): Response
{
    $submittedToken = $request->getPayload()->get('token');

    // 'delete-item' это то же значение, что используется в шаблоне для генерирования токена
    if ($this->isCsrfTokenValid('delete-item', $submittedToken)) {
        // ... сделайте что-то, вроде удаления объекта
    }
}

В качестве альтернативы вы можете использовать атрибут IsCsrfTokenValid в действии контроллера:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
// ...

#[IsCsrfTokenValid('delete-item', tokenKey: 'token')]
public function delete(): Response
{
    // ... сделать что-то, вроде удаления объекта
}

Suppose you want a CSRF token per item, so in the template you have something like the following:

1
2
3
4
5
6
<form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post">
    {# the argument of csrf_token() is a dynamic id string used to generate the token #}
    <input type="hidden" name="token" value="{{ csrf_token('delete-item-' ~ post.id) }}">

    <button type="submit">Delete item</button>
</form>

Атрибут IsCsrfTokenValid также принимает объект Expression, оцененный по id:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
// ...

#[IsCsrfTokenValid(new Expression('"delete-item-" ~ args["post"].getId()'), tokenKey: 'token')]
public function delete(Post $post): Response
{
    // ... сделать что-то, вроде удаления объекта
}

7.1

Атрибут IsCsrfTokenValid был представлен в Symfony 7.1.

CSRF-токены и атаки сжатия со сторонних каналов

BREACH и CRIME - это эксплойты безопасности против HTTPS при использовании сжатия HTTP. Хакеры могут использовать информацию, упущенную во время сжатия, чтобы восстановить целевые части открытого текста. Чтобы ослабить эти атаки, и предупредить хакера от укгадывания CSRF-токенов, к началу токена добавляется рандомная маска, которая используется для его перемешивания.