Безопасность баз данных

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

Для получения или сохранения информации в базе данных, к ней нужно подключиться, послать запрос, обработать ответ и закрыть подключение. Сегодня для всего этого обычно используется структурированный язык запросов (Structured Query Language, SQL). Давайте посмотрим, как злоумышленник может поступить с SQL-запросом.

Как известно, PHP не может сам защитить базу данных. Следующие разделы являются введением в основы доступа и использования баз данных в скриптах на PHP.

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

Планирование баз данных

Первый шаг всегда - собственно создание базы данных, за исключением случаев использования чужих баз. Когда создается база данных, ей назначается владелец, который и вызвал команду создания. Обычно только один владелец ("суперпользователь") может делать что угодно с объектами внутри этой базы данных и для того, чтобы позволить другим пользователям использовать ее, им должны быть назначены права доступа.

Приложения никогда не должны подключаться к базе данных в роли ее владельца или "суперпользователя", поскольку в этом случае пользователи могут произвести любые действия вроде модификации схемы (к примеру, удаления таблиц) или удаления всего ее содержимого.

Можно создать различных пользователей баз данных для каждого необходимого действия приложений, очень сильно ограничивая доступ последних к объектам базы данных. Требуемые права должны назначаться однократно, их использования в других местах приложения нужно избегать. Это значит, что если злоумышленник получит доступ с использованием той или иной учетной записи, он сможет получить лишь тот доступ, которым обладала использованная часть программы.

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

Подключение к базе данных

Можно устанавливать подключения с помощью SSL для шифрования соединений клиент-сервер, что дает повышенную безопасность. А можно использовать ssh для шифрования сетевых соединений между клиентами и сервером баз данных. Любой из этих способов сильно усложняет отслеживание и получение информации из сетевого траффика.

Хранение зашифрованных данных

SSL/SSH защищает данные только по пути от клиента к серверу, но не данные, хранимые в базе данных. SSL - лишь сетевой протокол.

Когда злоумышленник получает доступ к вашей базе данных в обход Web-сервера, хранимые важные данные могут быть получены и использованы, за исключением случая, когда информация защищена в самой базе данных. Шифрование - весьма хороший прием в этом случае, но такой способ поддерживают ныне лишь очень немногие системы управления базами данных.

Самый легкий путь в этом случае - создать свою собственную систему шифрования, а затем использовать ее в скриптах на PHP. PHP способствует такому подходу благодаря наличию специфичных раширений, таких, как Mcrypt и Mhash, охватывающих большой ряд алгоритмов шифрования. Программа шифрует сохраняемые данные и расшифровывает получаемые. Для детального описания схем шифрования смотрите ссылки.

В случае скрытых данных, где не требуется их исходный вид (к примеру, для отображения), можно использовать хеширование. Известным примером хеширования является сохранение в базе данных хеша MD5 от пароля вместо самого пароля. Для подробного описания смотрите crypt() и md5().

Пример 5-5. Использование хешированных паролей

// сохраняем хеш от пароля
$query  = sprintf("INSERT INTO users(name,pwd) VALUES('%s','%s');",
            addslashes($username), md5($password));
$result = pg_exec($connection, $query);

// проверяем корректность введенного пользователем пароля
$query = sprintf("SELECT 1 FROM users WHERE name='%s' AND pwd='%s';",
            addslashes($username), md5($password));
$result = pg_exec($connection, $query);

if (pg_numrows($result) > 0) {
    echo "Добро пожаловать, $username!";
}
else {
    echo "Введен неверный пароль для $username.";
}

Внедрение в SQL

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

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

При недостаточной проверке вводимых данных и соединении с базой данных на правах суперпользователя злоумышленник может создать нового суперпользователя в базе данных.

Пример 5-6. Разделение результата запроса по страницам и... создание суперпользователей (PostgreSQL и MySQL)

$offset = argv[0]; // внимание! нет проверки данных!
$query  = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
// в PostgreSQL 
$result = pg_exec($conn, $query);
// в MySQL
$result = mysql_query($query);
Обычно пользователи используют кнопочки "следующая" и "предыдущая", где $offset внедрен в URL. Программа считает, что $offset - число. Однако, кто-нибудь может попытаться внедриться путем добавления urlencode()-кодированных данных в URL

// в случае PostgreSQL
0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
    select 'crack', usesysid, 't','t','crack'
    from pg_shadow where usename='postgres';
--

// в случае MySQL
0;
UPDATE user SET Password=PASSWORD('crack') WHERE user='root';
FLUSH PRIVILEGES;

Если это случится, программа предоставит ему доступ суперпользователя. Заметим, что 0; служит для того, чтобы задать корректное смещение для исходного запроса и завершить его.

Замечание: Обычная практика - заставить транслятор SQL проигнорировать остаток запроса разработчика с помощью обозначения начала коментария SQL --.

Существует путь получения паролей через ваши страницы поиска. Все, что нужно злоумышленнику - это одна не обработанная должным образом переменная, используемая в SQL-запросе. Использоваться могут команды WHERE, ORDER BY, LIMIT и OFFSET запроса SELECT. Если ваша база данных поддерживает конструкцию UNION, злоумышленник может добавить к исходному запросу еще один - для получения паролей. В этом случае поможет хранение зашифрованных паролей.

Пример 5-7. Вывод статей... и паролей (любой сервер баз данных)

$query  = "SELECT id, name, inserted, size FROM products
                  WHERE size = '$size'
                  ORDER BY $order LIMIT $limit, $offset;";
$result = odbc_exec($conn, $query);
Статическая часть запроса может быть совмещена с другим запросом SELECT, который выведет все пароли:

'
union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
--

Если подобный запрос (использующий ' и --) будет задан в одной из переменных, используемых $query, то атака будет успешной.

Запросы SQL "UPDATE" также могут быть использованы для атаки на базу данных. Эти запросы также подвержены опасности "обрезки" и добавления новых запросов. Но здесь злоумышленник работает с командой SET. В этом случае необходимо знание некоторой информации о структуре базы данных для удачной модификации запроса. Такая информация может быть получена путем изучения названий переменных форм или просто подбором. В конце концов, не так уж и много имен придумано для полей пользователей и паролей.

Пример 5-8. От сброса пароля до получения привилегий... (любой сервер баз данных)

$query = "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
Злоумышленник посылает значение ' or uid like'%admin%'; --, в переменную $uid для изменения пароля администратора или просто устанавливает $pwd в "hehehe', admin='yes', trusted=100 " (с завершающим пробелом) для получения прав. Запрос будет искажен так:

// $uid == ' or uid like'%admin%'; --
$query = "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%'; --";

// $pwd == "hehehe', admin='yes', trusted=100 "
$query = "UPDATE usertable SET pwd='hehehe', admin='yes', trusted=100 WHERE ...;"

А вот пугающий пример того, как на некоторых серверах баз данных могут быть выполнены команды уровня операционной системы:

Пример 5-9. Атака на операционную систему сервера баз данных (сервер MSSQL)

$query  = "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);
Если злоумышленник пошлет значение a%' exec master..xp_cmdshell 'net user test testpass /ADD' -- в $prod, то $query будет выглядеть так:

$query  = "SELECT * FROM products
                    WHERE id LIKE '%a%'
                    exec master..xp_cmdshell 'net user test testpass /ADD'--";
$result = mssql_query($query);

Сервер MSSQL выполняет все команды SQL, включая команду добавления нового пользователя в локальную базу данных пользователей. Если это приложение было запущено, как sa и служба MSSQLSERVER имеет достаточно прав, злоумышленник будет иметь учетную запись для доступа к этой машине.

Замечание: Некоторые из вышеперечисленных примеров привязаны к конкретному серверу баз данных. Но это вовсе не означает, что подобная атака невозможна на другое программное обеспечение. Ваш сервер баз данных тоже так или иначе будет уязвим для непредвиденных атак.

Техника защиты

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

Большинство атак построены на использовании кода, который писался без учета соображений безопасности. Никогда не доверяйте введенным данным, особенно если они идут с клиентской стороны, пусть даже и из поля отметки, скрытого поля или записи cookie. Первый пример показывает, к чему может привести подмена этих данных.

  • Никогда не соединяйтесь с базой данных в роли суперпользователя или владельца. Всегда используйте специальных пользователей с минимумом прав.

  • Проверяйте ввод на совпадение типа данных с требуемым. PHP включает в себя большое количество проверочных функций, от самых простейших из разделов "Функции для работы с переменными" и "Функции обработки символьного типа", (к примеру is_numeric() и ctype_digit() соответственно) до регулярных выражений Perl ("Регулярные выражения, совместимые с Perl").

  • Если программа ожидает число, проверяйте данные с помощью is_numeric(), или просто изменяйте тип с помощью settype(), или даже используйте численное представление, выданное sprintf().

    Пример 5-10. Более безопасная разбивка на страницы

    settype($offset, 'integer');
    $query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
    
    // отметим %d в строке форматирования, использование %s бесполезно
    $query = sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;",
                     $offset);

  • Предваряйте любой нечисловой ввод, передаваемый в базу данных, функциями addslashes() или addcslashes(). В первом примере показано, что кавычек в статической части запроса недостаточно.

  • Не выводите никакой информации о структуре базы данных ни коим образом. Для описания подробностей смотрите раздел "Отчеты об ошибках" и "Функции обработки и учета ошибок".

  • Можно использовать хранимые процедуры и заданные расположения для того, чтобы отвязать обращение к данным от программы так, чтобы пользователи не имели прямого доступа к таблицам и представлениям, но этот вариант имеет собственные проблемы.

Кроме того, ведите журнал операций в программе или собственно в базе данных, если это поддерживается. Ведение журнала не предотвратит внедрения, но оно поможет определить, какая часть программы была поставлена под удар. Сам журнал бесполезен - полезна хранимая в нем информация. Чем ее больше - тем лучше.