CSRF – Spring Security в деталях

CSRF – Spring Security в деталях

Александр Косарев
Видео CSRF - Spring Security в деталях

Наверняка каждый разработчик веб-приложений или сайтов на практике сталкивался с защитой от CSRF-атак, предоставляемой фреймворками и библиотеками. В фреймворке Spring Security защита от этого вида эксплойтов тоже присутствует, однако многие начинающие разработчики предпочитают отключать её, не пытаясь разобраться в природе CSRF-атак, способах защиты от них и правильном использовании средств защиты.

В этой статье будет описан принцип действия CSRF-атаки и продемонстрированы несколько вариантов приведения таких атак. Затем будут перечислены признаки уязвимости веб-приложений перед CSRF-атаками и способы защиты от них. После этого будет подробно описана защита от CSRF-атак, предоставляемая Spring Security: компоненты, реализующие её, и их настройка. В завершение статьи будет описано несколько сценариев использования защиты от CSRF-атак в разных видах веб-приложений: в сайтах со статическими страницами, в сайтах с асинхронными запросами и в одностраничных веб-приложениях (SPA, PWA).

Принцип действия CSRF-атаки

Для сохранения данных между запросами и сессиями на стороне браузера в протоколе HTTP предусмотрены куки. Куки часто используются для хранения сессионных данных пользователей, так, например, Apache Tomcat устанавливает куки JSESSIONID, по которым он может определить HTTP-сессию пользователя. Установленные сайтом куки передаются в заголовках любых запросов к сайту, будь то запрос на получение изображения или файла каскадных таблиц стилей или будь то запрос какой-нибудь HTML-страницы.

Таким образом, если на странице одного сайта размещено, допустим, изображение со второго сайта, то при открытии этой страницы браузер отправит запрос на получение изображения со второго сайта, в заголовках которого среди прочих будут и заголовки Cookie для второго сайта. Да, первый сайт и его клиентский код не получат доступ к этим куки, но этого и не требуется для проведения атаки.

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

Схематичное изображение атаки

Признаки уязвимости к CSRF-атакам

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

Наличие сессионных данных в файлах куки

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

Использование неподходящих HTTP-методов

Все ресурсы браузеры запрашивают при помощи метода GET, будь то HTML-страница, файл каскадных таблиц стилей, изображение или шрифт. Метод GET относится к так называемым "безопасным" методам, среди которых кроме него есть HEADOPTIONS и TRACE, эти методы должны использоваться исключительно для получения информации, но не для совершения каких-либо действий на сайтах. В противном случае это может в какой-то степени упростить задачу злоумышленнику - вместо размещения формы с кнопкой для отправки нежелательного запроса, он может использовать имитацию встраивания элементов HTML-страницы:

<link rel="stylesheet" href="https://example.com/change-password?new-password=evil-ways">

Всё коварство такой имитации встраивания элементов заключается в том, что браузеры выполняют GET-запросы для получения соответствующих ресурсов автоматически без участия пользователей.

Неправильные настройки CORS

Настройки CORS, не ограничивающие список отправляющих запросы сайтов, методы, заголовки и аутентификационные данные делают осуществимыми CSRF-атаки при помощи клиентского кода JavaScript, размещённого на сайте злоумышленника и исполняемого браузером. Более того, такие настройки позволяют злоумышленнику написать код, позволяющий проверить наличие пользовательской HTTP-сессии на атакуемом сайте и скорректировать поведение вредоносного сайта и даже атакуемого пользователя.

Варианты проведения CSRF-атак

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

Отправка межсайтового запроса при помощи формы

Самый простой способ реализации CSRF-атаки - при помощи формы:

<form method="post" action="https://example.com/change-password">
    <input type="hidden" name="new-password" value="evil-ways">
    <button type="submit">Выиграть 1 000 000!</button>
</form>

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

Имитация встраивания в HTML-страницу

Если на атакуемом сайте используется GET-метод для выполнения действий, то задача злоумышленника упрощается - теперь он может использовать имитацию встраивания изображений, файлов каскадных таблиц стилей или JavaScript для совершения запросов к атакуемому сайту:

<link rel="stylesheet" href="https://example.com/change-password?new-password=evil-ways">

Скорее всего, пользователь даже не поймёт, что произошло что-то не то.

Отправка асинхронного межсайтового запроса

Использование неограниченных или слабых настроек CORS могут привести к тому, что злоумышленник может разместить на своем сайте клиентский код, который отправит нежелательный запрос к атакуемому сайту без ведома пользователя, даже если используются правильные HTTP-методы:

fetch("https://example.com/change-password", {
    method: "POST",
    credentials: "include",
    body: ...
})

Способы защиты от CSRF-атак

Защитить своё веб-приложение можно при помощи CSRF-токенов, отправляемых вместе с запросами, использованием правильных HTTP-методов и строгими настройками CORS.

Использование CSRF-токенов

Для защиты от CSRF-атак при выполнении любого действия в веб-приложении должны использоваться CSRF-токены. Серверная часть приложения должна сгенерировать токен, который будет ассоциироваться с HTTP-сессией пользователя, и передать его клиентской части. Клиентская часть при выполнении действия должна передавать этот токен в запросе: в строке, теле или заголовке запроса (но не в куки, тогда весь смысл CSRF-токена теряется). Серверная часть при выполнении действия должна провалидировать полученный CSRF-токен.

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

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

Кроме этого во избежание уязвимостей BREACH и CRIME желательно реализовать маскировку или шифрование CSRF-токена при передаче его от клиента к серверу таким образом, чтобы зашифрованный CSRF-токен был уникален. Про это я рассказывал в статье "Spring Security: Маскировка CSRF-токена".

Использование подходящих HTTP-методов

Для выполнения действий в веб-приложениях должны использоваться соответствующие методы: в классических веб-приложениях - это POST, а в REST API ещё и PUTPATCH и DELETE.

По умолчанию настройки защиты от CSRF-атак в Spring Security учитывают то, что методы GETHEADOPTIONS и TRACE являются безопасными, и запросы, использующие данные методы, не защищаются от CSRF-атак, следовательно, использовать их для совершения действий небезопасно.

CORS

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

Spring Security

Защита от CSRF-атак является частью библиотеки spring-security-web в Spring Security, так как специфична для веб-приложений. Основные компоненты, реализующие защиту от CSRF-атак: CsrfFilterCsrfTokenRequestHandlerCsrfTokenRepository и CsrfToken.

Компоненты защиты от CSRF-атак

CsrfFilter

Фильтр CsrfFilter - точка входа для защиты от CSRF-атак. Данный фильтр определяет, нужно ли применять защиту от CSRF-атак к поступившему запросу, и если да, то при помощи CsrfTokenRepository он получает связанный с текущей HTTP-сессией CSRF-токен (ожидаемый) и валидирует полученный в запросе CSRF-токен при помощи CsrfTokenRequestHandler.

Если в CsrfTokenRepository отсутствует ожидаемый CSRF-токен, то он будет создан и сохранён для дальнейших запросов.

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

Обработка запроса

CsrfTokenRepository

Репозиторий CSRF-токенов CsrfTokenRepository нужен для хранения токенов на серверной стороне приложения. Spring Security предоставляет две основные реализации: HttpSessionCsrfTokenRepository - для хранения в HTTP-сессии и CookieCsrfTokenRepository - для хранения CSRF-токена в куки-файлах браузера. По умолчанию используется HttpSessionCsrfTokenRepository, в то время как CookieCsrfTokenRepository может использоваться в случае отказа от использования HTTP-сессий. При необходимости вы можете реализовать собственный способ хранения CSRF-токенов.

Стоит отметить тот факт, что CookieCsrfTokenRepository и HttpSessionCsrfTokenRepository по умолчанию используют разные заголовки для передачи CSRF-токенов. Так CookieCsrfTokenRepository использует заголовок X-XSRF-TOKEN, а HttpSessionCsrfTokenRepository - X-CSRF-TOKEN, впрочем, вы можете самостоятельно указать заголовок, в котором должен передаваться CSRF-токен, и он может отличаться от указанных двух.

CsrfTokenRequestHandler

Обработчик запроса CsrfTokenRequestHandler должен сравнить полученный из репозитория CSRF-токен с токеном, полученным из запроса. Используемый по умолчанию в Spring Security XorCsrfTokenRequestAttributeHandler ожидает, что в запросе токен должен быть замаскирован случайным набором байтов при помощи исключающей дизъюнкции (XOR). Так же доступен CsrfTokenRequestAttributeHandler, работающий с незамаскированными CSRF-токенами.

CsrfToken и DeferredCsrfToken

CSRF-токен в Spring Security описывается интерфейсом CsrfToken, который декларирует три метода:

  • getToken() возвращает сам токен
  • getHeaderName() возвращает название HTTP-заголовка в котором CsrfTokenRequestHandler ожидает получить переданный от клиента CSRF-токен
  • getParameterName() возвращает название параметра запроса в котором CsrfTokenRequestHandler ожидает получить переданный от клиента CSRF-токен

DeferredCsrfToken реализует отложенную загрузку токена.

Настройка цепочки фильтров безопасности

Защита от CSRF-атак может быть настроена в цепочке фильтров безопасности при помощи метода HttpSecurity.csrf():

@Configuration
class SpringSecurityBeans {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    Set<String> csrfAllowedMethods = Set.of("GET", "HEAD", "TRACE", "OPTIONS");
    CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();
    return http
      .csrf(csrf -> csrf
        // Какие запросы должны быть защищены от CSRF-атак
        .requireCsrfProtectionMatcher(request ->
          !csrfAllowedMethods.contains(request.getMethod()))
        // Какие запросы не должны быть защищены от CSRF-атак
        .ignoringRequestMatchers("/api/**")
        // Репозиторий для хранения CSRF-токенов
        .csrfTokenRepository(csrfTokenRepository)
        // Обработчик CSRF-токенов
        .csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
        // Действие выполняемое после успешной аутентификации
        .sessionAuthenticationStrategy(
          new CsrfAuthenticationStrategy(csrfTokenRepository))
      )
      .build();
  }
}

Для настройки защиты от CSRF-атак доступны следующие методы:

  • Метод requireCsrfProtectionMatcher() определяет параметры запросов, которые должны обрабатываться фильтром CsrfFilter
  • При помощи метода ignoringRequestMatchers() можно определить параметры запросов, которые должны быть исключены из обработки фильтром CsrfFilter
  • Методом csrfTokenRepository() можно указать используемый репозиторий CSRF-токенов
  • Методом csrfTokenRequestHandler можно указать используемый обработчик CSRF-токенов
  • Методом sessionAuthenticationStrategy можно задать действие, выполняемое после успешной аутентификации, по умолчанию это смена CSRF-токена.

В целом можно использовать настройки защиты от CSRF-атак:

@Configuration
class SpringSecurityBeans {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
      .csrf(Customizer.withDefaults())
      .build();
  }
}

В настройках по умолчанию нет только игнорируемых запросов, остальные настройки соответствует описанным выше.

Сценарии использования защиты

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

Сайты со статическими страницами

В классических веб-приложениях со статическим содержимым HTML-страниц CSRF-токены можно вкладывать в запросы при помощи скрытого поля в форме. Движки шаблонов вроде Thymeleaf, Mustache или JSP могут получить доступ к параметрам CSRF-токена через свойство модели _csrf, ниже приведёт пример для Thymeleaf:

<form action="https://example.com/change-password" method="post">
    <input type="hidden" data-th-name="${_csrf.parameterName}"
           data-th-value="${_csrf.token}">
    <!-- Прочие элементы -->
</form>

Сайты с асинхронными запросами

Если веб-приложение предполагает использование асинхронных запросов для отправки данных серверной сторонне, то в этом случае параметры CSRF-токена можно вывести в каких-нибудь элементах HTML-страницы, например, в тегах <meta>:

<html>
<head>
    <meta name="csrf-header" data-th-content="${_csrf.headerName}">
    <meta name="csrf-parameter" data-th-content="${_csrf.parameterName}">
    <meta name="csrf-token" data-th-content="${_csrf.token}">
</head>
<body>
<!-- Прочие элементы -->
</body>
</html>

При отправке асинхронных запросов можно вкладывать CSRF-токен в заголовок:

const csrfHeader = document.querySelector("meta[name='csrf-header']").content;
const csrfToken = document.querySelector("meta[name='csrf-token']").content;
const headers = {};
headers[csrfHeader] = csrfToken;

fetch("https://example.com/change-password", {
    method: "POST",
    headers: headers
    // прочий код
})

Либо в тело запроса:

const csrfParameter = document.querySelector("meta[name='csrf-parameter']").content;
const csrfToken = document.querySelector("meta[name='csrf-token']").content;

const formData = new FormData();
formData.set(csrfParameter, csrfToken)

fetch("https://example.com/change-password", {
    method: "POST",
    body: formData
    // прочий код
})

Одностраничные сайты (SPA, PWA)

В случае с одностраничными веб-приложениями возникает проблема - такие приложения могут существовать отдельно от серверной части, и возможность вывести параметры CSRF-токена в исходной HTML-странице может отсутствовать. В этом случае клиентское веб-приложение должно каким-то образом получить CSRF-токен от серверной стороны.

Разработчики Spring Security предлагают вариант, при котором CSRF-токены хранятся в файлах куки браузера, но при этом они доступны клиентской стороне, что достигается отсутствием флага httpOnly у куки, хранящей CSRF-токен. Для этого нужно сконфигурировать защиту от CSRF-атак следующим образом:

@Configuration
class SpringSecurityBeans {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
      .csrf(csrf -> csrf
        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
      )
      .build();
  }
}

Клиентское веб-приложение должно получать CSRF-токен из куки перед отправкой запроса:

const csrfToken = document.cookie
    .split("; ")
    .find((row) => row.startsWith("XSRF-TOKEN="))
    ?.split("=")[1];

const headers = {};
headers["X-XSRF-TOKEN"] = csrfToken;

fetch("https://example.com/change-password", {
    method: "POST",
    headers: headers
    // прочий код
})

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

public class GetCsrfTokenFilter extends OncePerRequestFilter {

  private RequestMatcher requestMatcher = new AntPathRequestMatcher("/csrf");

  private CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();

  private ObjectMapper objectMapper = new ObjectMapper();

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                  FilterChain filterChain) throws ServletException, IOException {
    if (this.requestMatcher.matches(request)) {
      var token = this.csrfTokenRepository.loadToken(request);
      if (token == null) {
        token = this.csrfTokenRepository.generateToken(request);
        this.csrfTokenRepository.saveToken(token, request, response);
      }

      response.setStatus(HttpServletResponse.SC_OK);
      response.setContentType("application/json");
      this.objectMapper.writeValue(response.getOutputStream(), token);
      return;
    }

    filterChain.doFilter(request, response);
  }

  public void setRequestMatcher(RequestMatcher requestMatcher) {
    this.requestMatcher = requestMatcher;
  }

  public void setCsrfTokenRepository(CsrfTokenRepository csrfTokenRepository) {
    this.csrfTokenRepository = csrfTokenRepository;
  }

  public void setObjectMapper(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
  }
}

Фильтр необходимо будет зарегистрировать в цепочке фильтров безопасности:

@Configuration
class SpringSecurityBeans {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
      .addFilterAfter(new GetCsrfTokenFilter(), ExceptionTranslationFilter.class)
      .csrf(Customizer.withDefaults())
      .build();
  }
}

Альтернативно это можно сделать при помощи REST-контроллера или HandlerFunction. Теперь CSRF-токен можно получать при обращении к пути /csrf, результат будет выглядеть следующим образом:

{
  "headerName": "X-CSRF-TOKEN",
  "parameterName": "_csrf",
  "token": "d60f7036-1e2d-49e2-bac5-9cbf7ae876af"
}

На стороне клиентского веб-приложения теперь можно получать CSRF-токен перед отправкой запросов:

fetch("/csrf")
    .then(response => response.json())
    .then(csrf => {
        const headers = {};
        headers[csrf.headerName] = xorWithRandomBytes(csrf.token);

        return fetch("/change-password", {
            method: "POST",
            headers: headers
            // прочий код
        })
    })

Полезные ссылки


Report Page