SQL-инъекции (SQL injections, SQLi) — самый хорошо изученный и простой для понимания тип атаки на веб-сайт или веб-приложение. Тем не менее, он странным образом остается весьма распространенным и в наши дни. Организация OWASP (Open Web Application Security Project) упоминает SQL-инъекции в своем документе OWASP Top 10 2017 как угрозу номер один для безопасности веб-приложений, и вряд ли положение сильно изменилось за четыре года.
Попасться на 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).
В адресной строке браузера вводится запрос вида
https://example.com/showItem?item=1%20or%201=1
Это может заставить бэкенд приложения выполнить SELECT
-запрос с всегда истинным условием, что может привести к раскрытию несанкционированных данных.
Ключевое слово 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-запроса, во всяком случае напрямую. Достигается это использованием параметризированных (подготовленных) запросов.
Так, вместо подстановки (конкатенации) пользовательского ввода, надо использовать параметры, например, в случае 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-инъекцию. Давайте отметим основные тезисы:
Качественное видео с дополнительной информаций по теме:
Примеры уязвимостей и противодействия им на разных языках программирования, основанные на чудесном комиксе xkcd: bobby-tables.com.
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…