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

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

Как реализовать 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-токенов. Это уникальные маркеры, добавляемые в формы в качестве скрытых полей. Легитимный сервер проверяет их, чтобы убедиться, что запрос поступил от ожидаемого источника, а не от какого-то другого вредоносного сайта.

Анти-CSRF токенами можно управлять двумя способами: используя подход с состоянием - stateful, где токены хранятся в сессии и являются уникальными для пользователя и действия; или подход без состояния - stateless, где токены генерируются на стороне клиента.

Установка

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, является использование токенов CSRF без состояния , как объясняется ниже.

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

Этот атрибут также может быть применен к классу контроллера. Когда он используется таким образом, валидация токена CSRF будет применена ко всем действиям, определенным в этом контроллере:

1
2
3
4
5
6
7
8
9
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
// ...

#[IsCsrfTokenValid('the token ID')]
final class SomeController extends AbstractController
{
    // ...
}

Атрибут 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
{
    // ... сделать что-то, вроде удаления объекта
}

По умолчанию атрибут IsCsrfTokenValid выполняет проверку токена CSRF для всех методов HTTP. Вы можете ограничить эту валидацию определенными методами, используя параметр methods. Если запрос использует метод, не указанный в массиве methods, атрибут игнорируется для этого запроса, и никакой валидации CSRF не происходит:

1
2
3
4
5
#[IsCsrfTokenValid('delete-item', tokenKey: 'token', methods: ['DELETE'])]
public function delete(Post $post): Response
{
    // ... delete the object
}

7.1

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

7.3

Параметр methods был представлен в Symfony 7.3.

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

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

CSRF-токены без состояния

7.2

Анти-CSRF защита без состояния была представлена в Symfony 7.2.

Традиционно, CSRF-токены имеют состояние, что означает, что они хранятся в сессии. Однако некоторые ID токенов могут быть объявлены как без состояния, используя опцию stateless_token_ids. CSRF-токены без состояния включаются по умолчанию в приложениях, использующих Symfony Flex .

1
2
3
4
5
# config/packages/csrf.yaml
framework:
    # ...
    csrf_protection:
        stateless_token_ids: ['submit', 'authenticate', 'logout']

CSRF-токены без состояния обеспечивают защиту без опоры на сессию. Это позволяет вам полностью кешировать страницы, продолжая защищать от
CSRF-атак.

При валидации CSRF-токена без состояния, Symfony проверяет заголовки Origin и Referer входящего HTTP-запроса. Если ни один из них не совпадает с целевым происхождением приложения (то есть, его доменом), токен считается валидным.

Этот механизм полагается на то, что приложение может определить свое происхождение. Если вы находитесь за обратным прокси, убедитесь, что он правильно сконфигурирован. См. Как сконфигурировать Symfony, чтобы она работала за распределителем нагрузки или обратным прокси.

Использование ID токена по умолчанию

CSRF-токены с состоянием обычно имеют область действия на уровне формы или действия,
тогда как токены без состояния не требуют много идентификаторов.

В примере выше идентификаторы authenticate и logout указаны, потому что они используются по умолчанию в компоненте Symfony Security. Идентификатор submit включен, чтобы типы форм, определенные приложением, также могли использовать CSRF-защиту по умолчанию.

Следующая конфигурация применяется только к типам форм, зарегистрированным через автоконфигурацию (по умолчанию для ваших собственных сервисов), и устанавливает submit в качестве их идентификатора токена по умолчанию:

1
2
3
4
5
# config/packages/csrf.yaml
framework:
    form:
        csrf_protection:
            token_id: 'submit'

Формы, сконфигурированные с идентификатором токена, перечисленным в опции
stateless_token_ids выше, будут использовать CSRF-защиту без состояния.

Генерирование CSRF-токена с использованием Javascript

В дополнение к HTTP-заголовкам Origin и Referer, CSRF-защита без состояния также может валидировать токены, используя куки и заголовок (под названием csrf-token по умолчанию; см. справочник конфигурации CSRF ).

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

На стороне сервера валидация токена CSRF сравнивает значение в файле куки и заголовке. Эта защита от "двойной отправки" основана на политике одного происхождения браузера и дополнительно усиливается с помощью:

  • генерирования нового токена для каждой отправки (для предотвращения фиксации куки);
  • использования атрибутов куки samesite=strict и __Host- (для форсирования HTTPS и ограничения куки текущим доменом).

По умолчанию, фрагмент JavaScript Symfony ожидает, что скрытое поле CSRF будет иметь имя _csrf_token или будет содержать атрибут data-controller=«csrf-protection». Вы можете адаптировать эту логику к своим потребностям, если придерживаетесь того же протокола.

Чтобы предотвратить снижение уровня валидации, выполняется дополнительная проверка поведения: если (и только если) сессия уже существует, успешная "двойная отправка" запоминается и становится обязательной для будущих запросов. Это гарантирует, что после подтверждения эффективности
опциональной валидации куки/заголовка она остается действующей для этой сессии.

Note

Не рекомендуется форсировать валидацию "двойной отправки" для всех запросов, поскольку это может привести к ухудшению качества пользовательского опыта. Лучше
использовать описанный выше подход, который позволяет приложению плавно переходить к проверкам Origin / Referer, когда JavaScript недоступен.