Введение в CSRF-токены в Symfony

Источник: «Web Application Security: An Introduction to CSRF Tokens in Symfony»
Начиная знакомство с Symfony, часто приходится следовать документации, не всегда понимая значение тех или иных механизмов. В этом контексте стоит поближе рассмотреть токены CSRF, которые на первый взгляд могут показаться дополнительным усложнением, но их роль крайне важна для безопасности приложения. В этой статье я расскажу, почему CSRF-токен так важен, и проиллюстрирую это на примере популярной социальной сети, которая столкнулась с проблемами безопасности из-за недостаточной проверки этих токенов.

Оглавление

Что такое CSRF-токен

CSRF токены — это инструменты безопасности в веб-приложениях. Их основное назначение — предотвращение атак, при которых вредоносный скрипт на одной странице использует авторизацию пользователя на другом сайте для выполнения несанкционированных действий. CSRF-токен — это уникальная, трудно угадываемая строка символов, присваиваемая сессии пользователя. Когда пользователь отправляет форму, приложение проверяет соответствие токена, блокируя запросы, не содержащие его. История CSRF-токенов берет своё начало в начале 21 века, когда внимание стало уделяться растущим угрозам, связанным с безопасностью веб-приложений. Их разработка и внедрение стали ответом на необходимость более эффективной защиты от всё более совершенных методов атак.

Что такое CSRF уязвимость

Подделка межсайтовых запросов (CSRF) — это серьёзная уязвимость в веб-безопасности, позволяющая злоумышленникам побуждать пользователей к выполнению непредусмотренных действий. Эта уязвимость может быть особенно опасной, поскольку может привести к выполнению несанкционированных действий аутентифицированными пользователями без их ведома. CSRF-атаки используют доверие веб-приложения к браузеру пользователя, что делает эту проблему критически важной для веб-безопасности.

Атака CSRF требует соблюдения трёх ключевых условий: наличие соответствующего действия в приложении, которое может использовать злоумышленник, работа с сессиями на основе cookie, когда приложение полагается только на cookie сессии для идентификации пользователя, и отсутствие непредсказуемых параметров запроса в запросах, выполняющих это действие. Например, злоумышленник может обманом заставить пользователя изменить адрес электронной почты в учётной записи без его ведома.

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

Кроме того, важно понимать, что хотя CSRF в основном связан с обработкой сессий на основе cookie, он может возникать и в других контекстах, когда приложение автоматически добавляет некоторые учётные данные пользователя в запросы, например, при аутентификации HTTP Basic или аутентификации на основе сертификатов.

Для более глубокого понимания CSRF, механизма его работы и превентивных мер фонд OWASP предлагает исчерпывающие ресурсы и руководство по предотвращению CSRF.

Итак, как выглядит 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.

Дополнительные материалы

Предыдущая Статья

Функции высшего порядка в JavaScript

Следующая Статья

Инъекция зависимостей в командах Laravel Artisan