Pull to refresh

Митигация SQL-инъекций при работе с PostgreSQL

Level of difficultyEasy
Reading time6 min
Views5.6K

Привет, Хабр! Меня зовут Игорь, я занимаюсь разработкой серверной части в команде RuBackup.

В процессе своей работы мы с коллегами уделяем большое внимание вопросам безопасности наших приложений. SQL-инъекция — одна из самых серьезных угроз этой безопасности. Она заняла третье место в списке 25 самых опасных проблем в программном обеспечении за последние два года. Именно поэтому я решил собрать весь свой накопленный опыт и рассказать о митигации SQL-инъекций.

Наверняка многие из вас знают, что валидирование пользовательского ввода — краеугольный камень процесса безопасной разработки. С одной стороны, нам как разработчикам не хочется ограничивать пользователей в наборе таких входных данных, как символы и выражения, которые они могут использовать в работе, например, в паролях. С другой стороны, нельзя допустить выполнения в СУБД вредоносного кода, который приводит к SQL-инъекциям. А такие случаи время от времени происходят и становятся достоянием гласности. PostgreSQL дает разработчику возможность решить эту проблему экранированием потенциально опасных символов, превращая их в безопасные. Таким образом, для PostgreSQL будет вполне безвредно, если пользователь в качестве пароля использует строку "password' OR 1=1".

Для защиты от SQL-инъекций в прикладных библиотеках PostgreSQL libpq и libpqxx применяется техника «эскейпинг» или экранирование строки. Она заключается в том, чтобы убрать лишние символы разрыва строк в строках, содержащих специальные символы. С помощью этой функций символы удваиваются и более не считаются окончанием строки, а интерпретируются как обычные символы. Я буду рассматривать только библиотеку libpqxx, так как она, по сути, является С++ оберткой над более низкоуровневой С библиотекой libpq, где и реализованы все функции, о которых далее пойдет речь.

Экранирование строки 

Писать запросы в виде монолитных строк очень просто. Но иногда мы используем запросы вида: "SELECT id FROM user WHERE name = '" + name + "'", где используется переменная, содержащая пользовательский ввод.

Это опасно. Видите ошибку? Если переменная name будет содержать одинарные кавычки, то это может привести к SQL-инъекции, то есть у пользователя появится возможность ввести вредоносный код, например ".'; DROP TABLE user". Если вам повезет, это окажется просто неприятной ошибкой, которую вы обнаруживаете, когда переменная name имеет значение "Д'Артаньян".

Итак, вам нужно экранировать переменную name перед ее вставкой. Здесь кавычки и другие проблемные символы будут помечены как «это просто символ в строке, а не конец строки». В libpqxx есть несколько специальных функций, которые делают это за вас.

Пример SQL-инъекции

Чтобы понять, что такое SQL-инъекции и почему их следует предотвращать, воспользуемся популярным примером. Представьте, что вы используете следующую инструкцию SQL, где-нибудь в своей программе:

TX.exec(
        "SELECT number,amount "
        "FROM account "
        "WHERE allowed_to_see('" + userid + "','" + password + "')");

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

Теперь представьте, что будет, если пользователь на самом деле является злоумышленником, который знает (или может предположить) общую форму этого SQL-запроса и введет следующий пароль:

    x') OR ('x' = 'x

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

SELECT number,amount
    FROM account
    WHERE allowed_to_see('user','x') OR ('x' = 'x')

Это то, чего бы нам хотелось? Вероятно, нет! Выражение allowed_to_see() полностью обходится выражением "OR ('x' = 'x')", которое всегда истинно. Таким образом злоумышленник получит доступ ко всем учетным записям в базе данных!

Использование функций esc

Вот как вы можете устранить проблему в приведенном выше примере:

 TX.exec(
        "SELECT number,amount "
        "FROM account "
        "WHERE allowed_to_see('" + TX.esc(userid) + "', "
        "'" + TX.esc(password) + "')");

Теперь кавычки, встроенные в строку вредоносного кода, будут аккуратно экранированы. Они уже не смогут «вырваться» из заключенной в кавычки строки SQL, в которую они должны были входить:

SELECT number,amount
    FROM account
    WHERE allowed_to_see('user', 'x'') OR (''x'' = ''x')

Если внимательно посмотреть, то можно увидеть, что благодаря добавленным дополнительным эскейп-символам (в SQL одинарная кавычка экранируется путем ее удвоения) все, что мы получаем, — это очень странно выглядящая строка пароля, но не изменение в команде SQL.

Вы можете спросить, зачем же использовать метод класса транзакции для того, чтобы просто добавить к строке одинарные кавычки? Дело в том, что корректное экранирование зависит от свойств соединения, в первую очередь от текущей кодировки символов, и, следовательно, при игнорировании этих параметров могут выдаваться неправильные результаты. Таким образом, вызов метода экранирования требует наличие активного соединения с БД. Даже если вы используете объект lazy connection для экранирования строк, думая, что это быстрый и дешевый способ экранирования строки без установления соединения, то вызов esc все равно активирует соединение. Отсюда следует, что требование установления соединения с БД при экранировании — причина того, почему методы подобные esc не объявлены как const.

Также можно использовать метод quote для того, чтобы сразу и экранировать специальные символы, и заключить результирующую строку в одинарные кавычки. Например, метод quote преобразует строку abc в 'abc'

Экранирование двоичных данных 

Также библиотека libpqxx имеет и другие методы для экранирования, например, esc_raw. Этот метод призван экранировать двоичные данные для использования в качестве строкового литерала SQL в текущей транзакции. Необработанные двоичные данные обрабатываются иначе, чем обычные строки. Двоичные данные никогда не интерпретируются как текст, поэтому они могут вполне легитимно включать в себя одиночный байт или целые последовательности байтов, которые не являются допустимыми символами в текущей кодировке символов. Двоичная строка не заканчивается на первом нулевом байте, как в случае с текстовой строкой. Вместо этого такие строки могут содержать нулевые байты в любом месте. Если двоичная строка содержит байты, которые выглядят, как одинарные кавычки или что-то подобное, что может нарушить их использование в SQL-запросах, то такие символы будут заменены на специальные эскейп-последовательности. 

Для использования в приложении ранее экранированных двоичных данных требуется их обратное экранирование с помощью метода unesc_raw. Этот метод принимает двоичную строку, ранее экранированную средствами libpqxx, и возвращает восстановленную копию исходных двоичных данных.

Дополнительные методы экранирования

Для экранирования строк-идентификаторов библиотека libpqxx предлагает нам использовать метод quote_name. Это нужно в случае, если идентификатор, выбранный пользователем, может содержать специальные символы, которые в противном случае не интерпретировались бы синтаксическим анализатором SQL, как часть идентификатора. Также это может быть полезно, когда идентификатор содержит символы верхнего регистра, и его требуется сохранить для пользователя. Метод quote_name использует двойные кавычки в качестве символа экранирования. 

Отдельно хотелось бы отметить такую функцию, как esc_like. Она подготавливает строку для использования в LIKE части SQL-запроса. В данном случае обрабатываются спецсимволы % и _. Например, функция esc_like преобразует строку "a%b_c" в "a\\%b\\_c"

Уменьшение угрозы SQL-инъекций

Что же сделать, чтобы митигировать угрозу SQL-инъекций при разработке ПО? Один из подходов — сохранение чистоты типов данных. То есть если от пользователя приходит неотрицательное число, значит, его нужно хранить в коде как unsigned и в базе как integer. Таким образом, если мы это значение используем в запросе, нам достаточно преобразовать его к строке:

TX.exec( "SELECT name FROM account WHERE id="+std::to_string(id));

Также полезно при использовании ключевых слов SQL, таких как ASC, DESC, AND и других, которые могут приходить от пользователя, скажем, через URL, вообще не подставлять их в код, а использовать прием белых списков.  

const std::string order = (user_data == "DESC") ? "DESC" : "ASC";

Это же можно использовать и для идентификаторов в БД, если таких идентификаторов немного.

Для тех, кто интересуется данной темой и хочет узнать про SQL-инъекции чуть больше, я подобрал три материала: 

  1. SQL-инъекции' union select null,null,null --

  2. Чек-лист устранения SQL-инъекций

  3. Самый частый шаблон SQL инъекций в РНР — бесполезное экранирование символов

Tags:
Hubs:
Total votes 16: ↑4 and ↓12-8
Comments30

Articles

Information

Website
astragroup.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия