CSRF-токен

В этой статье рассмотрим, что такое Сross-Site Request Forgery и как включить в Spring Boot приложение CSRF-токен — защиту от этого мошенничества. Назвать этот токен можно было бы «анти-CSRF-токен».

Same Origin Policy и CSRF (cross-site request forgery)

Обычно запросы, сделанные в браузере с одного домена на другой, не проходят, поскольку браузер придерживается Same Origin Policy. Это политика безопасности, защищающая одни сайты от других.

Например, пусть пользователь случайно заходит на домен evil.com (мошеннический сайт) и щелкает там яркую кнопку, которая выполняет либо PUT-запрос, либо DELETE-запрос на bank.com. И так случайно получилось, что этот пользователь зарегистрирован  на bank.com и в браузере хранятся куки к нему. Благодаря политике безопасности браузера, ничего плохого не случится. Потому что браузер сначала вышлет  так называемый «preflight», то есть предварительный OPTIONS-запрос на bank.com с заголовком

Origin: evil.com

и затем отправит за ним настоящий PUT/DELETE-запрос, но только в том случае, если в ответе пришло разрешение на отправку запросов от evil.com. В противном случае в метод PUT/DELETE банковского сайта мы даже не попадем.

То есть предварительный запрос спрашивает у одного домена разрешение на отправку данных с другого. И чтобы bank.com (получатель запросов) отправил положительный ответ, программист bank.com должен знать домен evil.com (отправителя запросов). Тогда программист делает так, чтобы согласно спецификации CORS bank.com отправлял в ответе в заголовке Access-Control-Allow-Origin адрес evil.com. Это означает буквально «сайту evil.com можно ко мне обращаться». Как настроить CORS в Spring Boot читайте тут. CORS — это своего рода послабление Same Origin Policy.  В данной статье CORS нет.

Но есть запросы, которые отправляются сразу, без предварительного запроса, так называемые simple requests.  Это GET, HEAD, POST с определенным Content-Type. В частности, POST-запросы  с формы. Такой запрос сразу идет в контроллер. А учитывая, что куки браузер отправляет автоматически, запрос попадет в защищенный контроллер и выполнит действие на банковском сайте. Хотя ответ получить и распарсить нельзя, действие  будет выполнено.

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

Пример жульничества

Создадим два приложения на Spring Boot:

  • мишень для атаки, «банковское» приложение на порту 8080
  • и мошеннический сайт на порту 8081

Поскольку в примере мы будем делать запрос на «другой домен», пропишем для localhost:8080 новое имя в файле hosts:

C:\Windows\System32\drivers\etc\hosts

А именно, добавим в файл hosts строку:

127.0.0.1 bank-server

Теперь к приложению-мишени будем обращаться по адресу http://bank-server:8080/..вместо http://localhost:8080..

Приложение-мишень

Итак, пусть в нашем приложении есть контроллер и форма, с которой что-то добавляется:

Добавление
Добавление
@Controller
public class DocumentController {
    @PostMapping("/add")
    public String add(Document document, Model model) {

        System.out.println(document.getId()+" "+document.getText()+" added");

        model.addAttribute("document", document);
        model.addAttribute("message", "добавлено");
        System.out.println("PostMapping /add");
        return "add";
    }

    @GetMapping("/add")
    public String get() {
        System.out.println("GetMapping /add");
        return "add";
    }

    @GetMapping("/")
    public String main() {
        return "redirect:/add";
    }
}

Форма на Thymeleaf выглядит так:

<form th:action="@{/add}" th:method="post">
     <input type="text" th:value="${id}" name="id"></input>
     <input type="text" th:value="${text}" name="text"></input>

     <button type="submit">Добавить документ</button>
</form>

При этом адрес http://bank-server:8080/add защищен, доступ к нему возможен только благодаря куки JSESSIONID после входа с помощью формы логина.

В приложении задан единственный in-memory пользователь с именем user и паролем user:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder() {

        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.inMemoryAuthentication()
                .withUser("user")
                .password("user")
                .authorities("ROLE_USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().formLogin();

        http.csrf().disable();
    }
}

Выше прописано, что все запросы доступны только аутентифицированным пользователям (в том числе наша форма — ее получение по адресу /add методом GET и отправка методом POST). Форма находится в шаблоне add.html

Проверку CSRF-токена мы отключили выше отдельной строкой:

http.csrf().disable();

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

Мошенническое приложение

Второе приложение совсем простое, оно состоит из одного view с кнопкой атаки POST (полный код тут):

Сайт жуликов
Сайт жуликов
<html xmlns="http://www.w3.org/1999/xhtml">

....
<h1>Мошенническая кнопка:</h1>

<form method="post" action="http://bank-server:8080/add">
    <input type='hidden' name="id" value="5"/>
    <input type='hidden' name="text" value="document"/>

    <button type="submit">POST</button>

</form>


</body>
</html>

Кнопка отправляет форму со скрытыми полями: id=»1″ и  text=«document». Если в браузере мы залогинены в приложении http://bank-server:8080 и откроем мошеннический сайт localhost:8081 с вышеприведенной кнопкой и щелкнем ее, то сохраненный куки JSESSIONID отправляется тоже, и мы попадаем в защищенный метод add() контроллера DocumentController банковского сайта. Документ добавляется.

Защита с помощью CSRF-токена

Чтобы защититься от таких запросов, нужно включить CSRF-токен, то есть убрать строку отключения, которую мы добавили выше:

// http.csrf().disable();

Теперь когда пользователь логинится на сайт http://bank-server:8080, ему выделяется специальный CSRF-токен. Он хранится в сессии и должен отправляться как скрытое поле со всех форм (также он должен прилагаться в XMLHttpRequest-запросах PUT, DELETE, POST — это для JavaScript).  Выше мы показали только одну атаку, но при определенных настройках Spring MVC в определенных браузерах возможны и более сложные атаки. И хотя браузеры становятся все более безопасными, как и сам Spring MVC, все равно Spring Security имеет вот такое требование и для PUT, и для DELETE-запросов, хотя для них существуют «preflight» запросы от браузера. Но эти настройки CSRF-токена можно и поменять — убрать некоторые методы или некоторые url — сделать так, чтобы для них не требовался токен.

Итак, токен в POST-запросах сейчас требуется. Если оставить все как есть, то наше собственное банковское приложение не заработает — при попытке отправить форму получим 403.  Надо сделать так, чтобы выданный CSRF-токен отправлялся. Для этого добавим его как скрытое поле на форму в наш Thymeleaf-шаблон:

<form th:action="@{/add}" th:method="post">

        <input type="text" th:value="${id}" name="id"></input>
        <input type="text" th:value="${text}" name="text"></input>


    <!--csrf-токен добавить на форму-->
    <input type="hidden"
           name="${_csrf.parameterName}"
           value="${_csrf.token}"/>

        <button type="submit">Добавить документ</button>

</form>

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

Итоги

Исходный код обоих приложений есть на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *