Статьи

Spring State Security, часть 1: защита CSRF без сохранения состояния

Сегодня, когда архитектура RESTful становится все более и более стандартной, возможно, стоит потратить некоторое время на переосмысление ваших текущих подходов к обеспечению безопасности. В этой небольшой серии постов в блоге мы рассмотрим несколько относительно новых способов решения проблем безопасности, связанных с сетью, без учета состояния. Эта первая запись о защите вашего сайта от подделки межсайтовых запросов (CSRF).

Резюме: Что такое подделка межсайтовых запросов?

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

Другой сайт может злоупотребить этим, если браузер отправляет (межсайтовый) запрос на сайт, подвергающийся атаке. Например, включение некоторого javascript для создания тега POST для http://siteunderattack.com/changepassword?pw=hacked приведет к тому, что браузер выполнит этот запрос, прикрепив к запросу любые (аутентификационные) файлы cookie, все еще активные для этого домена!

Несмотря на то, что политика единого источника (SOP) не разрешает вредоносному сайту доступ к какой-либо части ответа. Как видно из приведенного выше примера, вред уже нанесен, если запрошенный URL-адрес вызывает какие-либо побочные эффекты (изменения состояния) в фоновом режиме.

Общий подход

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

Подходы без гражданства

1. Переключитесь на полный и правильно разработанный REST API на основе JSON.

Политика единого происхождения допускает только HEAD / GET и POST для нескольких сайтов. POST могут быть только одним из следующих типов mime: application / x-www-form-urlencoded, multipart / form-data или text / plain. На самом деле нет JSON! Теперь, учитывая, что GET никогда не должны вызывать побочные эффекты в каком-либо должным образом спроектированном API на основе HTTP, это позволяет вам просто запретить любые не-JSON POST / PUT / DELETE, и все в порядке. Для сценария с загрузкой файлов (multipart / form-data) все еще необходима явная защита CSRF.

2. Проверьте заголовок HTTP Referer.

Подход, описанный выше, может быть дополнительно уточнен путем проверки наличия и содержимого заголовка Referer для сценариев, которые все еще восприимчивы, например, POST-данных multipart / form-data. Этот заголовок используется браузерами, чтобы указать, какая именно страница (URL) инициировала запрос. Это можно легко использовать для проверки ожидаемого домена для сайта. Обратите внимание, что при выборе такой проверки вы никогда не должны разрешать запросы без присутствующего заголовка.

3. Клиентские сгенерированные CSRF-токены.

Пусть клиенты сгенерируют и отправят одно и то же уникальное секретное значение как в Cookie, так и в пользовательском HTTP-заголовке Учитывая, что веб-сайту разрешено только чтение / запись Cookie для своего собственного домена, только реальный сайт может отправлять одинаковые значения в обоих заголовках. Используя этот подход, все, что должен сделать ваш сервер, — это проверить, равны ли оба значения без сохранения состояния для каждого запроса!

Реализация

Сосредоточив внимание на третьем подходе к явной, но без сохранения состояния безопасности на основе токенов CSRF, давайте посмотрим, как это выглядит в коде с использованием Spring Boot и Spring Security.

В Spring Boot вы получаете хорошие настройки безопасности по умолчанию, которые вы можете настроить, используя свой собственный конфигурационный адаптер. В этом случае все, что нужно, это отключить поведение csrf по умолчанию и добавить собственный StatelessCSRFFilter:

Настроить защиту CSRF

01
02
03
04
05
06
07
08
09
10
11
@EnableWebSecurity
@Order(1)
public class StatelessCSRFSecurityConfig
        extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().addFilterBefore(
            new StatelessCSRFFilter(), CsrfFilter.class);
    }
}

А вот реализация StatelessCSRFFilter:

Пользовательский фильтр CSRF

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class StatelessCSRFFilter extends OncePerRequestFilter {
 
    private static final String CSRF_TOKEN = "CSRF-TOKEN";
    private static final String X_CSRF_TOKEN = "X-CSRF-TOKEN";
    private final RequestMatcher requireCsrfProtectionMatcher = new DefaultRequiresCsrfMatcher();
    private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
         
        if (requireCsrfProtectionMatcher.matches(request)) {
            final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN);
            final Cookie[] cookies = request.getCookies();
 
            String csrfCookieValue = null;
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals(CSRF_TOKEN)) {
                        csrfCookieValue = cookie.getValue();
                    }
                }
            }
 
            if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) {
                accessDeniedHandler.handle(request, response, new AccessDeniedException(
                        "Missing or non-matching CSRF-token"));
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
 
    public static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
        private final Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
 
        @Override
        public boolean matches(HttpServletRequest request) {
            return !allowedMethods.matcher(request.getMethod()).matches();
        }
    }
}

Как и ожидалось, версия без состояния не делает намного больше, чем простая функция equals () для обоих значений заголовка.

Реализация на стороне клиента

Реализация на стороне клиента также тривиальна, особенно при использовании AngularJS. AngularJS уже поставляется со встроенной поддержкой CSRF-токенов. Если вы укажете ему, из какого куки следует читать, он автоматически поместит и отправит свое значение в произвольный заголовок по вашему выбору. (Браузер позаботится об отправке самого заголовка cookie.)

Вы можете переопределить имена по умолчанию AngularJS (XSRF вместо CSRF) для них следующим образом:

Установите правильные имена токенов

1
2
$http.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
$http.defaults.xsrfCookieName = 'CSRF-TOKEN';

Кроме того, если вы хотите сгенерировать новое значение токена для запроса, вы можете добавить пользовательский перехватчик в $ httpProvider следующим образом:

Перехватчик для создания куки

01
02
03
04
05
06
07
08
09
10
11
12
13
14
app.config(['$httpProvider', function($httpProvider) {
    //fancy random token, losely after https://gist.github.com/jed/982883
    function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e16]+1e16).replace(/[01]/g,b)};
 
    $httpProvider.interceptors.push(function() {
        return {
            'request': function(response) {
                // put a new random secret into our CSRF-TOKEN Cookie before each request
                document.cookie = 'CSRF-TOKEN=' + b();
                return response;
            }
        };
    });   
}]);

Вы можете найти полный рабочий пример для игры на github .
Убедитесь, что у вас установлен gradle 2.0, и просто запустите его, используя «gradle build», а затем «gradle run». Если вы хотите поиграть с ним в вашей IDE, например eclipse, выберите «gradle eclipse» и просто импортируйте и запустите его изнутри вашей IDE (сервер не нужен).

отказ

Иногда классические CSRF-токены ошибочно считаются решением против переигровки или атак грубой силы. Подходы без сохранения состояния, перечисленные здесь, не охватывают этот тип атаки. Лично я считаю, что оба типа атак должны быть охвачены на другом уровне, например, с использованием https и ограничения скорости. Что я считаю обязательным для любого ввода данных на общедоступном веб-сайте!

Ссылка: Spring State Security, часть 1: защита без сохранения CSRF от нашего партнера JCG Роберта ван Вейверена в блоге JDriven .