Что такое SQL-инъекции и как им противостоять
SQL-инъекции (SQL injections, SQLi) — самый хорошо изученный и простой для понимания тип атаки на веб-сайт или веб-приложение. Тем не менее, он странным образом остается весьма распространенным и в наши дни. Организация OWASP (Open Web Application Security Project) упоминает SQL-инъекции в своем документе OWASP Top 10 2017 как угрозу номер один для безопасности веб-приложений, и вряд ли положение сильно изменилось за четыре года.
Попасться на SQL-инъекцию — это все равно что в преферансе вистовать «стоя» на девятерной и в первый ход зайти с «голой» семерки или нарваться на «детский мат» в шахматах. Первое случилось с автором этих строк на первом курсе КПИ и стоило ему стипендии за месяц, а второе как минимум дважды происходило на официальных турнирах, согласно онлайн-базе данных. Итак, что же делает этот классический хакерский трюк таким живучим? Давайте разбираться.
Содержание:
Источник уязвимости — язык SQL
Атака
Реальные примеры
Защита от SQL-инъекций
Заключение
Источник уязвимости — язык SQL
Почему вообще возможны SQL-инъекции?
Когда пользователь вводит и отправляет данные на веб-сайте, эти данные попадают в веб-приложение, которое в свою очередь использует эти данные при доступе к БД.
Допустим, у нас есть веб-сайт онлайн-магазина, и пользователь вводит название продукта в строке поиска. Для обращения к данным в реляционных БД используется SQL (structured query language) — специальный язык, похожий на естественный (английский) язык. Этот язык стандартизирован (самый последний стандарт — SQL:2008), и основные его команды одинаковы для разных производителей СУБД — Microsoft, Oracle, MySQL, PostgreSQL и других.
Например:
CREATE TABLE Users ( Id int PRIMARY KEY, Name varchar(100) NOT NULL, CreationDate datetime NOT NULL)
Этот запрос (команды в SQL называют запросами, англ. Queries) создает в базе данных таблицу Users (пользователи) с тремя полями — целочисленным идентификатором (Id), именем (Name) и датой создания записи о пользователе (CreationDate).
DROP TABLE Users
Этот запрос удаляет таблицу с пользователями из базы данных (не данные, т.е. строки из таблицы, а саму таблицу; для удаления строк используется запрос DELETE — смотрите ниже):
SELECT
Id,
Name
FROM
Users
WHERE
CreationDate > '2021-01-01'
Читает всех пользователей, созданных после 1 января 2021 года (в СУБД MS SQL Server).
DELETE FROM Users WHERE Id > 10
Удаляет всех пользователей с идентификатором большим 10
UPDATE Users SET CreationDate = '2020-12-01'
Устанавливает для всех (поскольку нет условия WHERE) пользователей дату создания в 1 декабря 2020 года.
Итак, веб-приложение (веб-сайт) для доступа к своим данным использует параметры, введенные пользователем на сайте.
Например, если пользователь ищет на нашем сайте веб-магазина ноутбуки и вводит слово «ноутбук» в строке поиска, то соответствующий запрос может иметь (упрощенный) вид:
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE 'ноутбук%'
Оператор LIKE и символ подстановки “%” (wildcard character) используются для задания условия по подстроке.
Соответственно, фрагмент программы, который формирует этот запрос для выполнения, в простейшем случае формируется из шаблона команды SELECT с подставленным значением пользовательского ввода (C#):
var selectProductQuery =
@"
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE '" + productName + "%'";
Атака
Теперь предположим, что пользователь — злоумышленник, и вводит в строке поиска не название продукта, а вредоносный фрагмент SQL-кода, например:
a'; DROP TABLE Products; --
Тогда результирующий запрос будет иметь вид двух последовательных команд и одного комментария:
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE '%a';
DROP TABLE Products;
-- %'
Комментарии в SQL имеют вид:
/*это многострочный*/комментарий--однострочный комментарий
Итак, что мы видим? Введенный пользователем текст в форме поиска превратил SQL-команду выбора продуктов из таблицы в два запроса: первый — бессмысленный, а второй — вредоносный, удаляющий таблицу продуктов из базы и делающий наше приложение (веб-сайт) нежизнеспособным.
Разумеется, это упрощенный пример, который предполагает, что команда, состоящая из нескольких запросов, будет выполнена как последовательность этих запросов, что данные пользователя не валидируются веб-фреймворком и так далее, но суть любой SQL-инъекции заключается именно в этом: злоумышленник внедряет («впрыскивает» — отсюда и название атаки «инъекция») вредоносный код в обычную форму ввода или в строку адреса, вынуждая веб-приложение произвести саморазрушительные действия или предоставить доступ внешнему атакующему к несанкционированным данным.
Реальные примеры
Наиболее часто применяются два вида атак SQL injection: Boolean-атака (Boolean Based SQLi) и UNION-атака (UNION Based SQLi).
Boolean-атака
В адресной строке браузера вводится запрос вида
https://example.com/showItem?item=1%20or%201=1
Это может заставить бэкенд приложения выполнить SELECT-запрос с всегда истинным условием, что может привести к раскрытию несанкционированных данных.
UNION-атака
Ключевое слово UNION используется для объединения результатов двух и более запросов в один результат.
Например, у нас есть таблица продуктов:
Id |
Name |
Price |
| 1 | Ноутбук | 29000 |
| 2 | Карандаш | 2 |
Выполнив запрос:
SELECT
Id,
Name,
Price
FROM
Products
UNION
SELECT
NULL,
CURRENT_USER,
NULL
Мы получим такой результат (для БД MS SQL Server):
UNION, злоумышленник может попытаться атаковать страницу поиска продуктов:
https://example.com/showProduct?id=1′ union select NULL,CURRENT_USER,NULL —
Что при определенном везении может раскрыть ему секретную информацию, такую как имя базы данных, имя пользователя, от которого происходит подключение к БД, и так далее. Понятно, что результат такой атаки может иметь катастрофические последствия для веб-приложения: раскрытие данных пользователей, полное разрушение приложения и его данных.
Защита от SQL-инъекций
Параметризуйте это
Самое главное правило — данные, присылаемые пользователем, не должны участвовать в формировании текста SQL-запроса, во всяком случае напрямую. Достигается это использованием параметризированных (подготовленных) запросов.
Так, вместо подстановки (конкатенации) пользовательского ввода, надо использовать параметры, например, в случае C# вместо фрагмента, рассмотренного ранее:
var selectProductQuery =
@"
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE '" + productName + "%'";
command.CommandText = selectProductQuery;
var reader = command.ExecuteReader();
Надо использовать следующий код:
command.CommandText =
@"
SELECT
Id,
Name,
Price
FROM
Products
WHERE
Name LIKE @p_productName";
command.Parameters.AddWithValue("p_productName", productName);
var reader = command.ExecuteReader();
В этом случае, какой бы текст пользователь не ввел в поле поиска продукта, приложение будет искать этот текст в качестве имени продукта, и если в тексте содержится нерелевантный фрагмент (например, с SQL-выражениями), никакой инъекции не произойдет, и продукт просто-напросто не будет найден.
Используйте хранимые процедуры
С технической точки зрения, это правило идентично предыдущему: пользовательский ввод не используется при динамической генерации SQL-запроса. Код хранимой процедуры неизменный и хранится в самой СУБД, а не в коде приложения.
Используйте белый список валидации
В некоторых случаях невозможно использовать параметризацию запросов.
Например, имя таблицы, из которой происходит выборка (SELECT), не может быть параметром, и в этом случае сам текст запроса формируется в зависимости от ввода пользователя.
В таком случае необходимо ограничить список допустимых значений, которые могут прийти от пользователя («белый список»).
Например, если из выпадающего списка веб-формы приходит значение Customers, то мы производим выборку из таблицы Customers, а если значение Supplier» — то из таблицы Suppliers.
Но если придет значение System_Users, которого приходить не должно, то это, скорее всего, значит, что мы имеем дело с злоумышленником, который с помощью Swagger или подобной программы пытается проверить наше приложение на прочность:
switch (param)
{
case "Customers":
tableName = "Customers";
break;
case "Suppliers":
tableName = "Suppliers";
break;
default:
throw new InputValidationException("Unexpected value.");
}
Валидируйте пользовательский ввод
Вообще в работе с пользовательским вводом руководствуйтесь принципом «пользователь — всегда потенциальный злоумышленник». Бэкенд не имеет права слепо доверять ничему, что приходит с клиента, даже если клиентское приложение валидирует пользовательский ввод. Злоумышленник может использовать Swagger, автоматические скрипты и другие средства преодоления клиентской валидации.
Поменьше привилегий
Системный пользователь (системная учетная запись), которая осуществляет доступ к данным, должна иметь как можно меньше привилегий на сервере.
Не может быть и речи о том, чтобы этой учетной записи было позволено читать и создавать файлы, не относящиеся к приложению, и производить другие критические с точки зрения безопасности действия.
Проверяйтесь
Всегда полезно проверять свое приложение на устойчивость, в том числе по отношению к SQLi-атакам. Одна из мощнейших и старейших утилит, предназначенных для поиска и устранения SQLi-уязвимостей — https://sqlmap.org/.
Заключение
В статье мы рассмотрели самый простой и самый распространенный тип атаки на веб-сайт — SQL-инъекцию. Давайте отметим основные тезисы:
- Суть атаки заключается в попытке злоумышленника внедрить вредоносный SQL-код через легальный канал ввода (веб-форма, адресная строка браузера).
- Наиболее эффективный способ защиты от SQL-инъекции — не использовать пользовательский ввод при построении SQL-запроса, а только в качестве значения параметров.
- Всегда полезно валидировать (проверять) пользовательский ввод на стороне бэкенда. Пользователь — всегда злоумышленник.
- Sqlmap — проверенное средство выявления SQLi-уязвимостей, и регулярное «обследование» им своего веб-приложения можно только приветствовать.
Ссылки
Качественное видео с дополнительной информаций по теме:
Примеры уязвимостей и противодействия им на разных языках программирования, основанные на чудесном комиксе xkcd: bobby-tables.com.



Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: