Як реалізувати 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
{
// ... зробити щось, наприклад, видалити обʼєкт
}
Припустимо, вам потрібен токен 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>
Цей атрибут також може бути застосовано до класу контролера. Коли він використовується так, валідація токена 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 недоступний.