SQL-инъекции считаются одной из наиболее опасных уязвимостей в онлайн-приложениях. Именно поэтому о них должен знать буквально каждый работающий с базами данных специалист, а статей об SQL-инъекциях как таковых очень и очень много.
К старту курса "Этичный хакер" мы решили поделиться именно этой статьёй, потому что она не только предлагает памятку по инъекциям, но и знакомит с платформой snyk.io — сканером на уязвимости, который для проектов с открытым исходным кодом можно использовать совершенно бесплатно, а значит, он будет полезен для пет-проектов многих разработчиков и читателей Хабра. По этой причине публикация попала и в хаб Программирование.
Существуют различные типы атак с использованием SQL-инъекций, но, как правило, все они имеют схожую причину. Происходит конкатенация (добавление) ненадёжных данных, введённых пользователем, к строке запроса. В результате введённые пользователем данные могут изменить само первоначальное назначение данного запроса.
Например, SQL-инъекции могут быть такими:
добавление к фразе where булева оператора, который делает это условие всегда выполняющимся ' OR 1=1;
удаление части запроса путём ввода строчных комментариев --;
завершение исходного запроса и запуск нового запроса '; DROP TABLE USERS;
соединение данных из нескольких таблиц с помощью оператора UNION.
В этой памятке я подробно рассмотрю 8 оптимальных методов, которые можно применять в любом приложении в целях предотвращения атак через SQL-инъекции. Итак, давайте начнём и сделаем ваше приложение действительно защищённым от SQL-инъекций.
Не полагайтесь на проверку вводимых данных на стороне клиента.
Ограничивайте полномочия пользователя базы данных.
Используйте предварительно подготовленные опраторы и параметризацию запросов.
Сканируйте код, чтобы обнаружить уязвимость к SQL-инъекциям
Используйте слой ORM.
Не полагайтесь на чёрные списки.
Выполняйте проверку вводимых пользователем данных.
С осторожностью используйте хранимые процедуры.
1. Не полагайтесь на проверку вводимых данных на стороне клиента
Проверка вводимых данных на стороне клиента может быть очень полезна. Благодаря ей можно предотвратить отправку в систему недопустимых данных. Однако, к сожалению, такая проверка хорошо работает только для пользователей, не имеющих злого умысла, которые намерены использовать систему строго по её прямому назначению. Выдача пользователю мгновенной подсказки о недопустимости конкретного значения — это очень полезная возможность, делающая систему более удобной для пользователей. Поэтому использование проверок вводимых данных на стороне клиента рекомендуется для повышения качества работы клиентов.
Но применительно к SQL-инъекциям это не тот метод защиты, на который действительно можно положиться. Проверку вводимых данных на стороне клиента легко можно отключить, изменив некоторый код javascript, загруженный в ваш браузер. Кроме того, в архитектуре клиент-сервер очень легко выполнить простейший HTTP-вызов серверного приложения, используя в нём параметр, вызывающий SQL-инъекцию. Также это можно сделать, используя различные инструментальные средства, такие как postman, или старые добрые команды curl.
Следует выполнять проверку на серверной стороне желательно как можно ближе к источнику данных. В рассматриваемом случае — там, где вы создаёте SQL-запрос. Все данные, передаваемые со стороны клиента, должны рассматриваться как потенциально опасные. Ввиду вышеизложенного возлагать надежды на проверку вводимых данных на стороне клиента в целях защиты от SQL-инъекций — плохая идея.
2. Ограничивайте полномочия пользователя базы данных
Как уже было упомянуто, существует несколько типов атак с использованием SQL-инъекций. Некоторые из них более опасны, чем другие. Представьте, к примеру, что мой SQL-запрос имеет вид "SELECT * FROM USER WHERE USERID = '" + userid +"'". Инъекция " foo' OR '1'='1 " предоставит доступ ко всем пользователям, и это само по себе очень опасно. Однако инъекция " '; UPDATE message SET password = 'EVIL” приведет к ещё большим проблемам, поскольку злоумышленник в этом случае изменит все записи.
Создавая пользователя в базе данных для вашего приложения, вы должны подумать о его полномочиях. Действительно ли приложению необходима возможность считывать, записывать и обновлять все базы данных? Должны ли быть у него полномочия для выполнения команд truncate и drop в отношении таблиц? Если вы сами ограничите полномочия своего приложения при работе с базой данных, то таким образом можно снизить и потенциальный вред от SQL-инъекций. Наверное, правильным будет создание для приложения нескольких пользователей базы данных и назначение им определённых ролей. Проблемы с безопасностью, как правило, вызывают эффект цепной реакции, поэтому необходимо контролировать все звенья вашей цепи, чтобы предотвратить тяжёлые последствия.
3. Используйте предварительно подготовленные операторы и параметризацию запросов
Во многих языках программирования специально предусмотрены функциональные возможности, помогающие предотвратить SQL-инъекции. При создании SQL-запросов вы можете использовать заранее подготовленные операторы SQL для компоновки своих запросов. Применение подготовленного оператора позволяет выполнить параметризацию запроса. Параметризация запросов — это особая техника, позволяющая формировать выполняемые операторы SQL динамически. Вы создаёте базовый вопрос с определёнными плейсхолдерами (метками-заполнителями), а затем безопасно прикрепляете переданные пользователем параметры к этим плейсхолдерам.
Используя действительно подготовленный оператор и параметризованные запросы, вы заставляете саму базу данных заботиться об экранировании. Прежде всего на основе строки запроса с плейсхолдерами в ней создаётся план выполнения этого запроса. На втором шаге в базу данных передаются «ненадёжные» параметры. Но, поскольку план выполнения уже создан, эти параметры ни на что повлиять уже не могут. Это также предотвращает возможность инъекций.
Пример на Java:
String query = "SELECT * FROM USERS WHERE username LIKE ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, parameter);
ResultSet result = statement.executeQuery();
Пример на Python с коннектором MySQL:
cursor = conn.cursor(prepared=True)
params = ("foo",)
cursor.execute("SELECT * FROM USERS WHERE username = %s", params)
Пример на JavaScript с mysql2:
connection.query("SELECT * FROM USERS WHERE username = ?",[
req.body.username
],function(error, results){});
//emulates a prepared statement
//OR
connection.execute("SELECT * FROM USERS WHERE username = ?",[
req.body.username
],function(error, results){});
//prepared statement
Тем не менее существует несколько способов сделать это в JavaScript при использовании, например, базы данных MySQL. Учитывайте, что при использовании функции .query() подготовленный оператор не выполняется. В этом случае подстановка параметров осуществляется на стороне клиента, то есть происходит эмуляция подготовленного оператора. Чтобы сделать настоящий подготовленный запрос к базе данных, следует использовать функцию .execute().
4. Сканируйте код, чтобы обнаружить уязвимость к SQL-инъекциям
Писать собственный код совсем не сложно. Но при этом нередко случаются ошибки. Для проверки кода у вас могут быть предусмотрены такие процессы, как ревью кода и парное программирование. Но является ли тот, кто проверяет ваш код или программирует в паре с вами, также и специалистом в вопросах безопасности? Сможет ли он заметить в коде ошибку, опасную с точки зрения SQL-инъекций? В любом случае будет полезно автоматически проверять собственный код на предмет потенциальных угроз безопасности, таких как SQL-инъекции.
Используя такой инструмент SAST (технология статического тестирование безопасности приложений), как Snyk Code, вы можете автоматически проверять свой код и обнаруживать в нём угрозы безопасности, включая SQL-инъекции. Этот процесс можно легко автоматизировать в SDLC, например, подключив к Snyk ваш Git-репозиторий.
5. Используйте слой ORM
Также необходимо рассмотреть возможность использования уровня объектно-реляционного отображения (ORM). Слой ORM преобразует данные из базы данных в объекты, а также выполняет обратное преобразование. Применение библиотеки ORM уменьшает количество явных SQL-запросов, а значит, делает приложение менее уязвимым к SQL-инъекциям.
Одним из ярких примеров использования библиотек ORM является применение решений Hibernate для Java и Entity Framework для C#. Эти языки строго типизированы, а значит, в них есть возможность устанавливать соответствие между объектами и таблицами в базах данных. Таким образом, у вас есть возможность полностью отказаться от самостоятельного формирования каких-либо SQL-запросов.
Тем не менее проблема всё равно возникнет, если потребуется написать свои собственные запросы. В библиотеке Hibernate для Java предусмотрен даже собственный язык написания запросов HQL (Hibernate Query Language). При создании запросов на HQL опять же нужно помнить о возможных инъекциях и использовать функцию createQuery(), которая работает аналогично подготовленным операторам.
Для языка JavaScript также существуют хорошо известные библиотеки ORM, такие как sequelize. С помощью sequelize вы сможете определить, как значения будут отображаться в определённые типы в базе данных. Но давайте смотреть правде в глаза, в конечном счёте любой библиотеке ORM требуется преобразование её внутренних логических команд в операторы SQL. Мы верим, что в библиотеках экранирование параметров реализовано правильно.
Чтобы быть уверенным, что в вашей библиотеке ORM нет проблем с SQL-инъекциями, необходимо выполнить её сканирование с целью выявления известных уязвимостей. Работа с некорректной, устаревшей версией библиотек sequelize или hibernate может стать причиной больших проблем. Используя Snyk Open Source для проверки своего проекта, вы сможете защититься от скрытых SQL-инъекций в применяемых библиотеках, а также от многих других потенциальных угроз.
Использование Snyk совершенно бесплатно
Вы можете сразу приступать к работе со Snyk Open Source. Всё, что для этого нужно, — пройти регистрацию на получение бесплатного аккаунта.
6. Не полагайтесь на чёрные списки
Уверен, эта ситуация хорошо знакома большинству читателей, но я всё равно повторюсь. Пожалуйста, никогда не применяйте чёрные списки для используемых вами параметров. Подход с применением таких списков задаёт набор правил, определяющих уязвимые входные данные. В случае соответствия вводимых данных этим правилам запрос блокируется. Но если правила проверки окажутся недостаточно жёсткими, то введённые злоумышленником данные всё равно сработают. А если они, наоборот, будут слишком ограничивающими, то будет заблокирован ввод допустимых данных.
Например, мы можем блокировать все запросы, где содержится слово OR. Возможно, в этом даже есть какой-то смысл, но на практике оказывается, что Or — это очень часто встречающееся в Израиле имя. И это означает, что множество моих коллег будут блокироваться при попытке ввода своих имён. То же самое справедливо для одиночных кавычек '. Этот символ содержится в огромном количестве имён. Подумайте о пользователях с фамилиями O’Neill и O’Donnell. А ведь есть ещё и имена, такие как Dont’a.
7. Выполняйте проверку вводимых данных
Да, вы всегда, обязательно должны выполнять проверку вводимых данных! Хотя предварительно подготовленные операторы с параметризацией запросов и являются лучшей защитой против SQL-инъекций, всегда создавайте несколько уровней защиты. Подобно выдаче ограниченных полномочий пользователю базы данных, проверка ввода — это великолепный способ снизить потенциальные риски для вашего приложения в целом.
Также возможны такие ситуации, когда использование предварительно подготовленных операторов оказывается невозможным. Некоторые языки программирования могут не поддерживать этот механизм, а некоторые устаревшие системы баз данных могут не поддерживать ввод данных пользователями в качестве параметров. В таких случаях проверка вводимых данных становится приемлемой альтернативой.
Обязательно убедитесь, что проверка вводимых данных выполняется на основе белых списков, а не чёрных, как уже описывалось выше. Создайте правило, описывающее все разрешённые шаблоны, например с помощью регулярного выражения. Можно также использовать для этого хорошо поддерживаемую библиотеку. Используйте это правило в сочетании с предварительно подготовленными операторами с параметризацией запросов и вы обеспечите надежную защиту.
8. С осторожностью используйте хранимые процедуры
Многие верят, что работа с хранимыми процедурами — это хороший способ защититься от SQL-инъекций. Но это не всегда так. Подобно SQL-запросам, созданным в вашем приложении, хранимые процедуры также подвержены инъекциям со стороны злоумышленников.
И, так же как и в случае с SQL-запросами, вам следует использовать в хранимой процедуре параметризованные запросы вместо добавления параметров. SQL-инъекции в хранимой процедуре достаточно легко предотвратить. Но не пытайтесь сделать это в MySQL:
DELIMITER //
CREATE PROCEDURE `FindUsers`(
IN Username VARCHAR(50)
)
BEGIN
SET @Statement = CONCAT('SELECT * FROM User WHERE username = ', Username, ' );
PREPARE stm FROM @Statement;
EXECUTE stm;
END //
DELIMITER ;
Вместо этого лучше используйте в своих хранимых процедурах параметризованные запросы:
DELIMITER //
CREATE PROCEDURE `First`(
IN Username VARCHAR(50)
)
BEGIN
PREPARE stm FROM 'SELECT * FROM User WHERE username = ?';
EXECUTE stm USING Username;
END //
DELIMITER ;
Как показано выше, к хранимым процедурам применяются те же правила, что и к коду приложения. Способы применения хранимых процедур различаются в зависимости от используемой базы данных. Убедитесь, что вы хорошо представляете себе, как хранимые процедуры реализуются в используемой вами базе, и также будьте бдительны в отношении возможных инъекций. Хоть я и уверен, что лучше всего убрать всю логику внутрь вашего приложения, но хранимые процедуры также могут быть разумным решением, если используемый вами язык программирования не поддерживает работу с предварительно подготовленными операторами.
Узнайте, как прокачаться и в других специальностях или освоить их с нуля:
Другие профессии и курсы
ПРОФЕССИИ
КУРСЫ
gudvinr
1, 3, 8 — фактически одно и то же, только разными словами: "передавать пользовательские данные только через параметры в prepared statements"
2 — не связано напрямую с инъекциями, если первый пункт не реализован — не поможет
4 — реклама
5 — не решает проблем, если ORM криво написана + реклама
6, 7 — бесполезно если реализован первый пункт
sshikov
Не, ну почему пункт 2 не поможет? Он просто для другого, и да, напрямую не связан.
Если у юзера нет прав, кроме как на SELECT — удалить данные уже будет нельзя. Разве это не достижение?
Так что 2 пункт — он реально хорош, не сам по себе, но как практически обязательное мероприятие, которое нужно проделать на любой базе.
FanatPHP
Нет, не достижение.
Во-первых, он не соответствует заявленной теме поста, которая про предотвращение, а не про смягчение последствий.
Во-вторых, кража данных может нанести куда больший ущерб, чем их удаление.
В-третьих, все диванные теоретики, которые рекомендуют это "практически обязательное мероприятие", сами никогда в жизни его не применяли. А иначе знали бы, что оно требует накладных расходов, по объему сравнимых со всем кодом приложения. Вместо того, чтобы тратить силы на такое засовывание головы в песок, лучше потратить его на реальную защиту от инъекций.
sshikov
>Во-вторых, кража данных может нанести куда больший ущерб, чем их удаление.
Поэтому я и написал что в дополнение. А не вместо.
>В-третьих, все диванные теоретики, которые рекомендуют это «практически обязательное мероприятие», сами никогда в жизни его не применяли.
Я применял и применяю. У нас это, повторюсь, обязательное мероприятие. PCI DSS, знаете ли. Вы давно читали стандарт? У нас речь про деньги, данные карточек, и т.п. И поэтому вот это вот все — обязательно. А если вы никогда этого не видели — ну так у вас еще все впереди.
FanatPHP
И? Кража данных карточек лучше чем их удаление? Серьёзно?
sshikov
Блин, да где вы у меня такое нашли?
То что в статье написана по большей части ерунда — так я с вами совершенно согласен (с комментом ниже). Именно поэтому я общаюсь не с автором, а с комментаторами, то есть с вами. У вас есть свое видение, у меня свое, несколько отличное. Но в целом мне кажется что мы говорим про одно и тоже. И я сразу подчеркнул, что пункт 2 про другое, и является полезным дополнением, а собственно с иньекциями он реально не помогает (ну т.е. он не про предотвращение, а именно про смягчение возможных последствий).