SQL-инъекции#
SQL-инъекция (sqli) — это уязвимость безопасности, которая позволяет злоумышленнику вмешиваться в запросы к базе данных приложения, внедряя злонамеренный SQL-код через входные данные, меняя исходный SQL-запрос.
Проблема возникает, когда приложение «слепо» объединяет пользовательские данные с кодом SQL-запроса, не проверяя и не обрабатывая их.
Пример инъекции#
Исходный запрос:
SELECT * FROM users WHERE username = '[user_input_1]' AND password = '[user_input_2]'
Злоумышленник вводит в поле username специальную строку: OR 1=1 --. Поле password он может оставить пустым или заполнить чем угодно.
Злонамеренный ввод:
user_input_1: **admin' or 1=1 --**
user_input_2: **[любой]**
Результат:
SELECT * FROM users WHERE username = 'admin' or 1=1 --' AND password = 'text'
Чем опасны SQL-инъекции?#
Злоумышленник может:
Обойти аутентификацию.
Получить несанкционированный доступ к данным: читать, копировать, скачивать всю базу данных.
Изменять и удалять данные: удалять, изменять, добавлять записи.
Выполнять команды на сервере БД: в некоторых случаях атака может привести к полному захвату сервера.
Методы защиты#
Подготовленные выражения (Prepared Statement) - наиболее эффективный метод защиты. Это механизм выполнения SQL-запросов, при котором шаблон SQL-запроса и данные передаются отдельно. Это полностью исключает возможность SQL-инъекций. Предпочтительнее использовать именно этот метод защиты.
val svName = "admin"
val svPass = "qwerty1234"
SQL(s"SELECT * FROM users WHERE username = {sUserName} AND password = {sPassHash}")
.on("sUserName" -> svName, "sPassHash" -> svPass)
Использование функций экранирования. Если использовать подготовленные выражения не представляется возможным, необходимо воспользоваться функциями экранирования, они «обезвреживают» специальные символы:
escapeString(input: String, quoteValue: Boolean): String- для экранирования строки. Экранирует символы одинарной кавычки (“) в строке. ЕслиquoteValue = true, заключит значение в одинарные кавычки.
val sUserInput = SqlEscape.escapeString("' union select id from Btk_User --", false)
SQL(s"SELECT id FROM Bs_Goods WHERE sName = '$sUserInput' ")
escapeNumber(input: String): String- для экранирования числа. Удаляет спецсимволы („;“, „–“, „/“, „/“, пробелы).
val sIdGood = SqlEscape.escapeNumber("1 OR 1=1 --")
SQL(s"""SELECT id FROM Bs_Goods WHERE id = $sIdGood""")
escapeName(input: String): String- для экранирования имен сущностей, например имена таблиц и колонок. Экранирует одинарные кавычки („), удаляет спецсимволы';', '--', '/*', '*/', '(', ')'.escapeDate(input: String): String- для экранирования даты. Экранирует символы одинарной кавычки („).escapeJson(input: String): String- для экранирования json. Экранирует символы одинарной кавычки („).
Динамическое построение SQL#
Динамическое построение SQL реализовано через SQLBuilder — инструмент, который собирает текст SQL-запроса из отдельных частей. Вместо склеивания строк вручную разработчик последовательно указывает, что запрос должен выбрать, из какой таблицы получить данные и по какому условию их отобрать. В результате билдер возвращает обычную строку SQL; к БД он не подключается и запрос не выполняет.
Билдер нужен, когда меняется именно структура запроса: например, программная логика выбирает разрешенный набор колонок, таблицу или дополнительные условия. Если запрос всегда имеет одну и ту же структуру, достаточно обычного параметризованного SQL-запроса без билдера.
Явное правило применения: если программа выбирает таблицу или набор колонок из заранее разрешенных вариантов, используйте SQLBuilder. Например, имя таблицы передавайте в FROM через escape_next, как в примере ниже. Если меняются только значения параметров, передавайте их через .on(...) без билдера. Не передавайте в методы билдера произвольный SQL-фрагмент из пользовательского ввода: SQLBuilder не делает такой фрагмент безопасным.
Если sTableName подставить в SQL-строку напрямую, злоумышленник сможет передать вместо имени таблицы свой SQL-фрагмент и изменить запрос. Вызов escape_next.FROM(...) обрабатывает динамический фрагмент как имя таблицы, но sTableName всё равно должен выбираться из разрешенного набора.
Пример формирования запроса для получения класса по идентификатору. Условие id = {idClass} остается шаблоном, а значение idClass передается отдельно при выполнении запроса.
val sSql = SQLBuilder
.SELECT("id", "sName")
.FROM("Btk_Class")
.WHERE("id = {idClass}")
.toString
SQL(sSql)
.on("idClass" -> idClass)
После вызова toString переменная sSql содержит следующий текст:
SELECT id
, sName
FROM Btk_Class
WHERE (id = {idClass})
В данном примере toString возвращает текст запроса, а SQL(...).on(...) выполняет его с параметром. Значение idClass не становится частью SQL-строки, поэтому его нельзя подменить SQL-кодом.
Экранирование динамических фрагментов
Если в программе нужно добавить динамический фрагмент структуры запроса, перед соответствующим методом можно вызвать escape_next. Например, ниже в sTableName передается имя таблицы, которое программа получила из собственного заранее разрешенного набора.
val sSql = SQLBuilder
.SELECT("id", "sName")
.escape_next.FROM(sTableName)
.WHERE("id = {idClass}")
.toString
escape_next действует только на следующий вызов билдера. В примере он обрабатывает аргумент FROM функцией Gtk_SqlPkg.escapeName, после чего следующий фрагмент добавляется без такой обработки.
escape_next не заменяет параметризацию. Он удаляет отдельные специальные последовательности, но не проверяет SQL-выражение целиком и не передает значение как параметр. Поэтому им можно дополнительно обработать динамический фрагмент, но значения из формы, интеграции и других внешних источников всегда передавайте через параметры в .on(...).