Як реалізувати CSRF-захист

Дата оновлення перекладу 2025-01-16

Як реалізувати 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>

        <!-- some content here to distract the user -->
    </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
{
    // ... зробити щось, наприклад, видалити обʼєкт
}

Припустимо, вам потрібен токен CSRF для кожного елемента, тому в шаблоні у вас є щось на кшталт цього:

1
2
3
4
5
6
<form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post">
    {# аргумент csrf_token() - це динамічний id рядок, що використовується для генерування токена #}
    <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-токенів, до початку токена додається рандомна маска, яка використовується для його змішування.