Spring Security 4 + CSRF (добавление в Spring проект защиты от межсайтовой подделки запроса)

Здравствуйте!
Современное веб приложение считается уязвимым, если в нем отсутствует защита от Межсайтовой подделки запроса (CSRF).
В Spring Security 4.x она включена по умолчанию, поэтому при миграции с Spring Security 3.x на 4.x ее надо либо отключить
либо, правильнее и зачетнее, добавить в проект.
Собственно, сделал это в 10-минутном видео:
Ниже приведу основные моменты внедрения CSRF в проект:
- Использовать правильные HTTP запросы: по умолчанию CSRF защита отсутствует для запросов GET, HEAD, TRACE, OPTIONS. Это в частности означает, что для logout, если мы не хотим, чтобы злоумышленный сайт мог разлогинить пользователя, авторизованного в нашем приложении, требуется запрос POST.
- Во всех формах, где есть submit, добавить скрытое поле name=_csrf со значением csrfToken. Проще всего это сделать через Spring’s form tag library, который, кроме биндинга и валидации, при включенном csrf подставит в форму требуемое скрытое поле.
var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $(document).ajaxSend(function(e, xhr, options) < xhr.setRequestHeader(header, token); >);
@RestController @RequestMapping(value = AdminRestController.REST_URL, consumes = MediaType.APPLICATION_JSON_VALUE)
Ну и напоследок несколько ссылок на тему:
- Spring Security CSRF Protection
- Are JSON web services vulnerable to CSRF attacks
- Правило ограничения домена (SOP)
- Cross-origin resource sharing (CORS)
Как защитить Spring приложение, отключив CSRF-токен?
Всем доброго времени суток.
Я пишу Spring-boot (security) приложение и хочу использовать все инструменты веб-разработки. А именно: iframe и упрощённые ajax запросы. Во время разработки отключаю CSRF, добавляя в настройки http.csrf().disable() По хорошему CORS запросы настраиваются, но в некоторых случаях настроек мало. Например, когда нужно сделать sharing через iframe (пример: YouTube, Twitter и др.). И я решаю эту задачу путём банального добавления Principal в параметры каждого контроллера.
@GetMapping("/page") public @ResponseBody void page(Principal principal) < if(principal==null) return; String email = principal.getName(); Person pers = service.getUserByLogin(email); //some code. >
Вся логика приложения строится на том, что каждый запрос либо авторизованный, либо нет. Разумеется, у каждого пользователя свои связные данные (Linked data) и он не может получить доступ к личной информации других пользователей. Вопрос: На сколько удовлетворяет такое решение Same Origin Policy и на сколько это безопасно?
- Вопрос задан 04 дек. 2023
- 179 просмотров
Комментировать
Решения вопроса 0
Ответы на вопрос 2
calculator212 @calculator212
Ответ написан 05 дек. 2023
Нравится 2 2 комментария
My1Name @My1Name Автор вопроса
По второй ссылке хорошо описана ситуация и, в данном случае, мне необходимо позаботиться о «кликджекинге».
Когда сайт загружается в IFrame, все его кнопки становятся доступными и на сайте потенциального злоумышленника; любой щелчок считается действительным и для моего приложения. Это может быть фишинг и всё что угодно.
Возникает другой вопрос: Если добавить проверку референс getRequestURL(); в методы контролёра, такой запрос через iframe будет считаться внешним или локальным? Как узнать URL запроса если страница во фрейме?
calculator212 @calculator212
позаботиться о «кликджекинге»
Вроде достаточно настроить X-Frame-Options: тут
хз насколько актуально но вроде это просто делается в спринге
My1Name @My1Name Автор вопроса
Учитывая тот факт, что у меня не банковское приложение и финансовых рисков особо никаких нет, я не вижу причин запрещать пользователям встраивать личные страницы (или др. страницы сайта) в iframe. Главная задача в таком случае: ͟з͟а͟п͟р͟е͟т͟и͟т͟ь͟ ͟а͟в͟т͟о͟р͟и͟з͟и͟р͟о͟в͟а͟н͟н͟ы͟е͟ ͟з͟а͟п͟р͟о͟с͟ы͟. Для этого я разместил в шапку (в header на всех страницах сайта) незамысловатый скрипт:
const currentUrl = document.referrer; $.get('ajax/index', );
document.referrer — возвращает URL родительской страницы (которая загрузила iframe). Это работает, даже если родительский документ и iframe находятся на разных доменах. Как только начинается загрузка страницы, currentUrl отправляется в контроллер:
private @ResponseBody void checkReferrer(Principal principal, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException < String currentUrl = req.getParameter("ref"); currentUrl = currentUrl.substring(currentUrl.indexOf("/")+2); String localName = req.getServerName(); if (principal!=null && !currentUrl.startsWith(localName)) < req.getSession().invalidate(); req.logout(); Cookie [] cookies = req.getCookies(); if(cookies!=null) < for(Cookie c : cookies) < c.setValue(""); c.setPath("/"); c.setMaxAge(0); resp.addCookie(c); >> > >
Контроллер проверяет переданный URL и сравнивает его с именем сервера. Далее, если переданный адрес не включает в себя имя сервера и это ͟а͟в͟т͟о͟р͟и͟з͟и͟р͟о͟в͟а͟н͟н͟ы͟й͟ ͟з͟а͟п͟р͟о͟с͟, то происходит выход с системы и удаление всех куки. В настройках Spring-Security:
http.csrf() .disable().and() . frameOptions().sameOrigin();
Политика Same-Origin предотвращает доступ внешним скриптам к странице встроенной в iframe. А при отключении скриптов, любые запросы будут происходить от имени пользователя. То есть, програмно можно получить доступ только к своему аккаунту и только через главный вход (через iframe вход не получится). А если я что-то не учёл — критика в комментариях приветствуется.
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 выглядит так:
При этом адрес 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 (полный код тут):

.Мошенническая кнопка: