Защита веб-приложений Spring Boot

Эта статья относится к сайтам, созданным с помощью Spring Boot framework. Мы обсудим следующие четыре метода добавления дополнительных уровней безопасности в приложения Spring Boot: * Предотвращение SQL-инъекции с помощью параметризованных запросов * Проверка ввода параметров URL * Проверка ввода поля формы * Кодирование вывода для предотвращения отраженных атак XSS Я использую эти методы для своих веб-сайт Initial Commit [https://initialcommit.com], созданный с использованием Spring Boot, механизма шаблонов Thymeleaf, Apa

Эта статья относится к сайтам, созданным с помощью Spring Boot framework. Мы обсудим следующие четыре метода добавления дополнительных уровней безопасности в приложения Spring Boot:

  • Предотвращение внедрения SQL с помощью параметризованных запросов
  • Проверка ввода параметра URL
  • Проверка ввода поля формы
  • Кодирование вывода для предотвращения отраженных XSS-атак

Я использую эти методы для своего веб-сайта Initial Commit , который создан с использованием Spring Boot, механизма шаблонов Thymeleaf, Apache Maven и размещен на AWS Elastic Beanstalk.

При обсуждении каждого совета по безопасности мы сначала опишем вектор атаки, чтобы проиллюстрировать, как можно использовать соответствующую уязвимость. Затем мы расскажем, как обезопасить уязвимость и уменьшить вектор атаки. Обратите внимание, что есть много способов выполнить данную задачу в Spring Boot - эти примеры предложены, чтобы помочь вам лучше понять потенциальные уязвимости и методы защиты.

Предотвращение внедрения SQL с помощью параметризованных запросов

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

Один из распространенных способов, которым злоумышленник попытается внедрить SQL в ваше приложение, - это параметры URL-адреса, которые используются для создания SQL-запросов, которые отправляются в базу данных. Например, рассмотрим следующий пример URL-адреса:

 https://fakesite.com/getTransaction?transactionId=12345 

Допустим, существует конечная точка контроллера загрузки Spring, определенная в /getTransaction которая принимает идентификатор транзакции в параметре URL:

 @GetMapping("/getTransaction") 
 public ModelAndView getTransaction(@RequestParam("transactionId") String transactionId) { 
 
 ModelAndView modelAndView = new ModelAndView(); 
 
 sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = " + transactionId; 
 
 Transaction transaction = jdbcTemplate.query(sql, new TransactionRowMapper()); 
 
 modelAndView.addObject("transaction", transaction); 
 modelAndView.setViewName("transaction"); 
 
 return modelAndView; 
 } 

Обратите внимание, что оператор SQL в этом примере построен с использованием конкатенации строк. transactionId просто пристегивается после «WHERE» пункта используя + оператор.

Теперь представьте, что злоумышленник использует следующий URL-адрес для доступа к сайту:

 https://fakesite.com/getTransaction?transactionId=12345;+drop+table+transaction; 

В этом случае параметр URL-адреса transactionId (который определяется как String в нашем методе контроллера) используется злоумышленником для добавления в оператор "DROP TABLE", поэтому следующий SQL-код будет запущен для базы данных:

 SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = 12345; drop table transaction; 

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

Чтобы исправить ситуацию, мы можем использовать функцию, называемую параметризованными запросами . Вместо того, чтобы объединять наши динамические переменные непосредственно в операторы SQL, параметризованные запросы распознают, что передается небезопасное динамическое значение, и используют встроенную логику для обеспечения экранирования всего предоставленного пользователем содержимого. Это означает, что переменные, переданные через параметризованные запросы, никогда не будут выполняться как живой код SQL.

Вот версия уязвимых фрагментов кода выше, обновленная для использования параметризованных запросов:

 sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = ?"; 
 
 Transaction transaction = jdbcTemplate.query(sql, new TransactionRowMapper(), transactionId); 

Обратите внимание на замену оператора + transactionId непосредственно в операторе SQL. Они заменены знаком ? , который представляет собой переменную, которая будет передана позже. transactionId передается в качестве аргумента jdbcTemplate.query() , который знает, что все параметры, переданные в качестве аргументов, необходимо экранировать. Это предотвратит обработку любого пользовательского ввода базой данных как живого кода SQL.

Другой формат передачи параметризованных запросов в Java - NamedParameterJdbcTemplate . Это дает более четкий способ идентифицировать и отслеживать переменные, передаваемые через запросы. Вместо использования ? символ для идентификации параметров, NamedParameterJdbcTemplate использует двоеточие : за которым следует имя параметра. Имена и значения параметров отслеживаются в структуре карты или словаря, как показано ниже:

 Map<String, Object> params = new HashMap<>(); 
 
 sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = :transactionId"; 
 
 params.put("transactionId", transactionId); 
 
 Transaction transaction = jdbcTemplate.query(sql, params, new TransactionRowMapper()); 

Этот пример ведет себя идентично предыдущему, но он более популярен из-за ясности, которую он дает при идентификации параметров в операторе SQL. Это особенно верно в отношении более сложных операторов SQL, которые содержат большое количество ? которые необходимо проверить, чтобы убедиться, что они находятся в правильном порядке.

Проверка ввода параметра URL

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

Один из распространенных способов получения приложениями входных данных от пользователей - это непосредственно из строки URL-адреса в форме параметров URL-адреса. Образец URL-адреса, который мы использовали в предыдущем разделе, является примером передачи transactionId в качестве параметра URL-адреса:

 https://fakesite.com/getTransaction?transactionId=12345 

Предположим, мы хотим убедиться, что идентификатор транзакции является числом и находится в диапазоне от 1 до 100 000. Это простой двухэтапный процесс:

Добавьте @Validated к классу контроллера, в котором находится метод.

Используйте встроенные аннотации проверки непосредственно в @RequestParam в аргументе метода, как показано ниже:

 @GetMapping("/getTransaction") 
 public ModelAndView getTransaction(@RequestParam("transactionId") @min(1) @max(100000) Integer transactionId) { 
 // Method content 
 } 

Обратите внимание , что мы изменили тип transactionId к Integer из String , и добавил @min и @max аннотаций рядный с transactionId аргументом для обеспечения соблюдения указанного числового диапазона.

Если пользователь предоставляет недопустимый параметр, не соответствующий этим критериям, javax.validation.ContractViolationException которое можно обработать, чтобы представить пользователю ошибку, описывающую, что он сделал неправильно.

Вот несколько других часто используемых аннотаций ограничений, используемых для проверки параметров URL:

  • @Size : размер элемента должен находиться между указанными границами.
  • @NotBlank : элемент не должен быть NULL или пустым.
  • @NotNull : элемент не должен быть NULL.
  • @AssertTrue : элемент должен быть истинным.
  • @AssertFalse : элемент должен быть ложным.
  • @Past : элемент должен быть датой в прошлом.
  • @Future : элемент должен быть датой в будущем.
  • @Pattern : элемент должен соответствовать указанному регулярному выражению.

Проверка ввода поля формы

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

Обычно ввод поля формы передается от клиента к серверу через запрос POST. Поскольку данные формы обычно включают произвольный ввод пользователя, все данные поля ввода должны быть проверены, чтобы убедиться, что они не содержат вредоносных значений, которые могут нанести вред приложению или раскрыть конфиденциальную информацию.

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

 @Entity 
 public class Pet { 
 
 @Id 
 @GeneratedValue(strategy=GenerationType.IDENTITY) 
 private Integer id; 
 
 @NotBlank(message="Name must not be empty") 
 @Size(min=2, max=40) 
 @Pattern(regexp="^$|[a-zA-Z ]+$", message="Name must not include special characters.") 
 private String name; 
 
 @NotBlank(message="Kind must not be empty") 
 @Size(min=2, max=30) 
 @Pattern(regexp="^$|[a-zA-Z ]+$", message="Kind must not include special characters.") 
 private String kind; 
 
 @NotBlank(message="Age must not be empty") 
 @Min(0) 
 @Max(40) 
 private Integer age; 
 
 // standard getter and setter methods... 
 } 

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

Обратите внимание, что каждое поле имеет аннотации, которые определяют диапазон, в который должно входить поле. Кроме того, String (имя и тип) имеют @Pattern , которая реализует ограничение регулярного выражения, которое принимает только буквы и пробелы. Это предотвращает попытки злоумышленников использовать специальные символы и символы, которые могут иметь значение в контекстах кода, таких как база данных или браузер.

HTML-форма содержит соответствующие Pet , включая имя питомца, вид животного, возраст и может выглядеть примерно так:

Обратите внимание, что этот фрагмент HTML включает теги шаблона Thymeleaf для разметки HTML.

 <form id="petForm" th:action="@{/submitNewPet}" th:object="${pet}" method="POST"> 
 <input type="text" th:field="*{name}" placeholder="Enter pet name…" /> 
 
 <select th:field="*{kind}"> 
 <option value="cat">Cat</option> 
 <option value="dog">Dog</option> 
 <option value="hedgehog">Hedgehog</option> 
 </select> 
 
 <input type="number" th:field="*{age}" /> 
 
 <input type="submit" value="Submit Form" /> 
 </form> 

Когда поля формы заполнены и нажата кнопка «Отправить», браузер отправит запрос POST обратно на сервер в конечной точке «/ submitNewPet». Это будет получено методом @RequestMapping , определенным следующим образом:

 @PostMapping("/submitNewPet") 
 public ModelAndView submitNewPet(@Valid @ModelAttribute("pet") Pet pet, BindingResult bindingResult) { 
 
 ModelAndView modelAndView = new ModelAndView(); 
 
 if (bindingResult.hasErrors()) { 
 modelAndView.addObject("pet", pet); 
 modelAndView.setViewName("submitPet"); 
 } else { 
 modelAndView.setViewName("submitPetConfirmation"); 
 } 
 
 return modelAndView; 
 } 

Аннотация @Valid в аргументе метода будет обеспечивать выполнение @Valid Pet объекта домена Pet. bindingResult обрабатывается Spring автоматически и будет содержать ошибки, если какой-либо из атрибутов модели имеет проверки ограничений. В этом случае мы включаем простой вход, чтобы перезагрузить submitPet если ограничения нарушены, и отобразить страницу подтверждения, если поля формы действительны.

Кодирование вывода для предотвращения отраженных XSS-атак

Последняя тема безопасности, которую мы собираемся обсудить, - это кодирование вывода пользовательского ввода и данных, полученных из базы данных.

Представьте себе сценарий, в котором злоумышленник может передать значение в качестве входных данных через параметр URL, поле формы или вызов API. В некоторых случаях этот вводимый пользователем ввод может быть передан как переменная прямо обратно в шаблон представления, который возвращается пользователю, или он может быть сохранен в базе данных.

Например, злоумышленник передает строку, которая является допустимым кодом Javascript, например:

 alert('This app has totally been hacked, bro'); 

Давайте рассмотрим сценарии, в которых указанная выше строка сохраняется в поле базы данных как комментарий, который позже будет извлечен в шаблоне представления и отображен для пользователя в его интернет-браузере. Если переменная не экранирована должным образом, alert() фактически запустится как живой код, как только страница будет получена браузером пользователя - они увидят всплывающее предупреждение. Хотя этот код и раздражает, в реальной атаке этот код будет не предупреждением, а вредоносным сценарием, который может заставить пользователя сделать что-нибудь неприятное.

Фактически, вредоносный контент, предоставленный пользователем, не обязательно должен быть сохранен в базе данных, чтобы причинить вред. Во многих случаях вводимые пользователем данные, такие как имена пользователей, по существу возвращаются пользователю для отображения на странице, которую он посещает. По этой причине они называются «отраженными» атаками, поскольку злонамеренный ввод отражается обратно в браузер, где может нанести вред.

В обоих этих случаях динамический контент должен быть правильно закодирован на выходе (или экранирован), чтобы убедиться, что он не обрабатывается браузером как живой код Javascript, HTML или XML.

Этого можно легко добиться с помощью развитого механизма шаблонов, такого как Thymeleaf. Thymeleaf можно легко интегрировать в приложение Spring Boot, добавив необходимые зависимости файла POM и выполнив некоторые незначительные шаги настройки, которые мы здесь не будем вдаваться. th:text в Thymeleaf имеет встроенную логику, которая будет обрабатывать кодирование любых переменных, которые передаются в него, следующим образом:

 <h1>Welcome to the Site! Your username is: <span th:text="${username}"></span></h1> 

В этом случае, даже если username содержала вредоносный код, такой как alert('You have been hacked'); , текст будет просто отображаться на странице, а не выполняться браузером как живой код Javascript. Это связано с встроенной логикой кодирования Thymeleaf.

об авторе

Эта статья была написана Якобом Стопаком, консультантом по программному обеспечению и разработчиком, стремящимся помочь другим улучшить свою жизнь с помощью кода. Джейкоб является создателем Initial Commit - сайта, посвященного тому, чтобы помочь любознательным разработчикам узнать, как написаны их любимые программы. Его избранный проект помогает людям изучать Git на уровне кода.

comments powered by Disqus