К чему может привести усечение данных в SQL

Совсем недавно известный эксперт по безопасности Стефан Эссер (Stefan Esser) опубликовал в своем блоге статью, посвященную новой уязвимости, связанной с особенностями сравнения строк и автоматического усечения данных в MySQL. Именно Стефан обнаружил знаменитую дыру zend_hash_del_key_or_index, после чего в Сети появилось множество эксплоитов для популярных веб-приложений. Похоже в этот раз дело грозит обернуться теми же последствиями.

Как известно, любой столбец в таблице имеет свою максимальную длину. Вся суть уязвимости заключается в том, что при превышении этого значения MySQL отсекает оставшуюся часть данных, при этом не вызывая никаких предупреждений. Как отмечает Стефан, никаких дополнительных действий по настройке MySQL совершать не нужно, т.е. данные будут обрезаться по умолчанию, однако при испытании на своем веб-сервере с MySQL 5.0.20-nt, ожидаемого результата я не получил, хотя перед этим системные переменные я не переназначал. Действительно, запуск MySQL с системной переменной sql_mode, выставленной в STRICT_ALL_TABLES или STRICT_TRANS_TABLES, не позволит беспрепятственно записать данные в таблицу, если они превышают максимально допустимую длину, — в этом случае возникнет ошибка. Как выяснилось позже, мой локальный сервер MySQL был скофигурирован именно с STRICT_TRANS_TABLES, что поначалу вводило меня в заблуждение. Поэтому, если вы будете проводить испытания на своем компьютере, убедитесь, что MySQL запущен в нормальном режиме.

Чтобы понять каким образом можно воспользоваться данной особенностью MySQL, рассмотрим следующий пример:

<?php
include 'connect.php';
if(!isset($_POST['submit'])) {
?>
<h1>Registration</h1>
<form method="POST">
Username:
<input type="text" name="username" />
Password:
<input type="password" name="password" />
<input type="submit" name="submit" />
</form>	
<?php
} else {
	$username = mysql_escape_string($_POST['username']);
	$password = mysql_escape_string($_POST['password']);
	$r = mysql_query("SELECT * FROM users WHERE username='$username'");
	if(mysql_num_rows($r)) die("User already registered!");
	else {
		if(mysql_query("INSERT INTO users (username,passhash,status)
                                     VALUES ('$username','".md5($password)."','user')")) {
			echo "Successfully registered!";
		} else echo mysql_error();
	}
}
?>

Как несложно догадаться, — это скрипт регистрации пользователей. Предположим, что admin — это логин существующего пользователя со статусом администратора. Максимальная длина данных для столбца username — 16 байт. При регистрации пользователя с логином «admin » (с пробелом) запрос, проверяющий логин на существование, вернет данные уже существующего в базе данных пользователя, так как для MySQL «admin» и «admin » — это одно и то же строковое значение. Однако, если передать значение содержащее «admin» (5 байт) + 11 пробелов (уже 16 байт) + любое количество других символов (например просто «x» — итого 17 байт), то первый запрос вернет false, так как такого пользователя не существует, а вот второй успешно выполнится, при этом «x» будет отсечен, и в таблице users появится новая запись c логином admin и одинадцатью пробелами.

Дальнейшая реализация уязвимости зависит от способа осуществления авторизации пользователей. Если сначала отдельным запросом проверяется соответствие пароля, а затем уже вторым запросом извлекаются данные пользователя и впоследствии назначается сессия этого юзера, то уязвимость имеет место. Т.е. код примерно такой:

<?php
$userdata = null;
$r = mysql_query("SELECT username FROM users WHERE username = '$username'
                         AND passhash = MD5($password)");
if (mysql_num_rows($r)) {
   $r = mysql_query("SELECT * FROM users WHERE username = '$username'");
   $userdata = mysql_fetch_array($r);
   /* ... */
}
?>

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

Честно говоря, такую модель можно встретить довольно редко, однако уязвимость может возникнуть в самых неожиданных местах, так как веб-разработчики обычно мало знакомы с особенностями языков C и C++, на которых написаны интерпретатор PHP (вспомните про NULL-байт) и MySQL (тема этого поста)

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

Добавлено 08.09.2008

Как заметил Elekt, уникальные поля (UNIQUE KEY) неуязвимы перед SQL Column Truncation также как и поля с индексами.


7 комментариев

  1. Makaka, 27. августа 2008, 11:07

    Очень интересно !
    Хочу спросить если сайт работает на UTF-8 то считать нужно всеравно просто количество символов. т.е Мускул считает именно байты или он проверяет кодировку.
    Спасибо и большой респект

     
  2. Raz0r, 27. августа 2008, 13:21

    На стороне серверных веб-приложений проверять нужно длину строки по количеству символов, и кодировка здесь не имеет значения. MySQL производит сравнение строк не в двоичном формате, более того применяются очень мягкие правила (например `admin`=`admin `), так что максимальная длина данных для колонки соответствует количеству символов независимо от кодировки.

     
  3. Makaka, 28. августа 2008, 9:30

    А как быть с PHP ?
    Помоему строковые функции PHP не умеют работать с UTF-8
    mbstring ?…

     
  4. Raz0r, 31. августа 2008, 16:51

    >Помоему строковые функции PHP не умеют работать с UTF-8
    совершенно верно, в этом случае нужно использовать mbstring

     
  5.  

    […] Также читайте мою статью, посвященную SQL Column Truncation. […]

     
  6. Веля Солнышкин, 1. мая 2009, 23:54

    Если у меня VertrigoServ,то где я должен её переназначать ?
    Я имею ввиду переменную sql_mode oO

     
  7. Raz0r, 2. мая 2009, 15:49

    sql_mode меняется в my.cnf; где именно он находится в VertrigoServ я не знаю, так как ни разу с ним не сталкивался

     

Write a comment: