Введение в CSRF-токены в Symfony
Оглавление
Что такое CSRF-токен
CSRF токены — это инструменты безопасности в веб-приложениях. Их основное назначение — предотвращение атак, при которых вредоносный скрипт на одной странице использует авторизацию пользователя на другом сайте для выполнения несанкционированных действий. CSRF-токен — это уникальная, трудно угадываемая строка символов, присваиваемая сессии пользователя. Когда пользователь отправляет форму, приложение проверяет соответствие токена, блокируя запросы, не содержащие его. История CSRF-токенов берет своё начало в начале 21 века, когда внимание стало уделяться растущим угрозам, связанным с безопасностью веб-приложений. Их разработка и внедрение стали ответом на необходимость более эффективной защиты от всё более совершенных методов атак.
Что такое CSRF уязвимость
Подделка межсайтовых запросов (CSRF) — это серьёзная уязвимость в веб-безопасности, позволяющая злоумышленникам побуждать пользователей к выполнению непредусмотренных действий. Эта уязвимость может быть особенно опасной, поскольку может привести к выполнению несанкционированных действий аутентифицированными пользователями без их ведома. CSRF-атаки используют доверие веб-приложения к браузеру пользователя, что делает эту проблему критически важной для веб-безопасности.
Атака CSRF требует соблюдения трёх ключевых условий: наличие соответствующего действия в приложении, которое может использовать злоумышленник, работа с сессиями на основе cookie, когда приложение полагается только на cookie сессии для идентификации пользователя, и отсутствие непредсказуемых параметров запроса в запросах, выполняющих это действие. Например, злоумышленник может обманом заставить пользователя изменить адрес электронной почты в учётной записи без его ведома.
Для предотвращения CSRF-атак наиболее распространённым способом защиты является использование CSRF-токенов. Эти токены представляют собой уникальные, секретные и непредсказуемые значения, генерируемые на стороне сервера и проверяемые при каждом запросе пользователя, изменяющем состояние. Это гарантирует, что запрос исходит от легитимного источника. CSRF-токены должны быть уникальными для каждой пользовательской сессии, храниться в секрете и генерироваться безопасным методом. Очень важно обеспечить безопасную передачу токенов и не раскрывать их в логах сервера или URL-адресах.
Кроме того, важно понимать, что хотя CSRF в основном связан с обработкой сессий на основе cookie, он может возникать и в других контекстах, когда приложение автоматически добавляет некоторые учётные данные пользователя в запросы, например, при аутентификации HTTP Basic или аутентификации на основе сертификатов.
Для более глубокого понимания CSRF, механизма его работы и превентивных мер фонд OWASP предлагает исчерпывающие ресурсы и руководство по предотвращению CSRF.
- https://owasp.org/www-community/attacks/csrf
- https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- CSRF: Подделка межсайтовых запросов
- CSRF: Как предотвратить уязвимость
- CSRF: Обход проверки токена
- CSRF: Обход ограничений SameSite cookie-файлов
Итак, как выглядит CSRF атака
Представьте ситуацию, когда CSRF-атака заключается в изменении адреса электронной почты на сайте. Предположим, что на сайте плохо реализована функция изменения адреса электронной почты с помощью простого GET-запроса. Такой запрос может выглядеть следующим образом:
GET /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE
email=enemy@example.comКогда пользователь авторизуется на сайте, появляется возможность для манипуляций. Например, он получает от друга или даже находит на самом сайте ссылку на якобы изображение. Однако на самом деле эта ссылка — скрытый запрос на изменение адреса электронной почты. Он может выглядеть следующим образом:
<img src="http://vulnerable-website.com/email/change?email=enemy@example.com" width="0" height="0" />Перейдя по ссылке, пользователь не замечает никаких изменений — изображение не видно, поэтому он закрывает вкладку и продолжает пользоваться сервисом. Однако в фоновом режиме, незаметно для себя, он изменил адрес своей электронной почты на тот, который предоставил злоумышленник. Когда выйдя из системы снова пытается войти в неё, то обнаружит, что у него больше нет доступа, поскольку адрес электронной почты был изменён без его ведома.
В представленном случае список уязвимостей гораздо длиннее, например, уведомление клиента о смене адреса электронной почты вместе со ссылкой, подтверждающей действие, неправильный HTTP-метод, — но больше всего нас интересует отсутствие проверки CSRF-токена. Вся забава с токеном заключается в том, что во время отображения формы мы генерируем некоторое случайное значение, которое затем помещаем в сессию. Затем, во время отправки формы, мы ожидаем именно это значение, указанное в форме. У злоумышленника нет возможности узнать это значение. Потому что это всё равно что играть в угадай число, которое я задумал
. В этом и заключается весь секрет 🙂.
Как это делает Symfony
В Symfony управление токенами CSRF встроено в систему форм. Чтобы лучше понять этот процесс, давайте посмотрим, как Symfony подходит к этому вопросу, используя соответствующие технологии и практики. Во-первых, Symfony позволяет глобально включить или отключить генерацию CSRF-токенов для всех форм, установив соответствующее значение в конфигурации (framework.csrf_protection). Это удобное решение, позволяющее управлять безопасностью на уровне приложения. Более того, Symfony предлагает возможность настроить обработку CSRF-токенов для каждой формы отдельно, независимо от глобальных настроек. Пример конфигурации класса формы в Symfony выглядит следующим образом:
class MyType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'unique_form_identifier',
]);
}
}В этой конфигурации мы указываем, должна ли форма быть защищена CSRF-токеном, как будет называться поле формы, содержащее токен, и какой идентификатор токена будет использоваться. Установка уникального идентификатора токена (csrf_token_id) для каждой формы является рекомендуемой практикой, особенно если на сайте много форм. При генерации формы в представлении CSRF-токен должен быть добавлен в поле формы. Это можно сделать с помощью функции form_row:
{{ form_start(form) }}
{{ form_row(form.fieldName) }}
...
{{ form_row(form._token) }}
{{ form_end(form) }}Symfony также предлагает альтернативный метод внедрения CSRF-токена непосредственно в HTML, который может быть полезен, если вы не используете встроенную систему форм Symfony. Для этого используется функция csrf_token, генерирующая CSRF-токен:
<form action="..." method="post">
<input type="hidden" name="token" value="{{ csrf_token('unique_identifier') }}">
<button type="submit">Submit</button>
</form>Наконец, стоит упомянуть, что Symfony хранит CSRF-токены в пользовательской сессии. Это стандартное решение, сочетающее в себе безопасность и простоту использования. Для этого Symfony использует пакет symfony/security-csrf, содержащий реализации для генерации и хранения CSRF-токенов. Если вам интересно проанализировать код, то в нем есть несколько примечательных мест. Первое — это конфигурация CSRF, содержащаяся в FrameworkExtension. Здесь мы видим, что конфигурация токенов напрямую связана с сессиями и существованием класса CsrfTokenManager из другого пакета Symfony — symfony/security-csrf. А по указанной ниже ссылке вы можете увидеть конфигурацию CSRF в контексте Symfony Forms. И в самом конце я рекомендую вам взглянуть на пакет symfony/security-csrf. Вы можете увидеть две вещи: первая заключается в том, что генерация токена на самом деле является генерацией случайного значения:
// https://github.com/symfony/security-csrf/blob/7.0/TokenGenerator/UriSafeTokenGenerator.php
namespace Symfony\Component\Security\Csrf\TokenGenerator;
// ...
class UriSafeTokenGenerator implements TokenGeneratorInterface
{
// ...
public function generateToken(): string
{
// Generate a URI safe base64 encoded string that does not contain "+",
// "/" or "=" which need to be URL encoded and make URLs unnecessarily
// longer.
$bytes = random_bytes(intdiv($this->entropy, 8));
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
}Второй момент: по умолчанию и, по сути, единственным хранилищем CSRF, определённым в этом пакете, является хранилище сессий: https://github.com/symfony/security-csrf/tree/7.0/TokenStorage.