PHP в деталях

       

Пароль на страницу. Часть 2. Блокировка подбора (отредактированная)


DL
15.5.2001

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

Но сначала о блокировке подбора. Банальности, но всё-таки. Пароль длинной десять символов из букв латиницы и цифр - это очень много вариантов. Если подбирать пароль по 1000 000 вариантов в секунду, понадобится несколько тысяч лет. Но поскольку такую абракадабру запомнить сложно, мы чаще делаем пароль из осмысленных слов. Несколько лет назад оказалось, что большинство паролей можно подобрать при помощи словаря из 10 000 слов. В своё время в сети появился червь (вирус такой), который лазил по юниксовым серверам, используя их дырки в защите, и подбирал пароли привелигированых пользователей при помощи... системного орфографического словаря Юникса. Ничего таскать не надо было!

Каждый пользователь, пока он не ввёл правильный логин и пароль, считается злобным хакером. С чем же мы имеем дело, когда пользователь вводит что-либо неправильно?

1. забывчивость (на это на приличных сайтах есть формочка "забыл пароль", чтобы отправить на введёный в системных настройках email этот самый пароль)

2. баловство ("ибо нефиг")

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

4. DoS-атака (чтобы не перегрузить сервер, надо минимизировать действия, которые будет выполнять скрипт в таком случае)

Я долго думал, как можно вызвать перегрузку на сервере, если механизм защиты стоит на файлах. Оказалось, несложно (сколько это будет стоить - другой вопрос). Итак, допустим, сервер не выдержит, если скрипт будет пытаться 1000 раз в секунду открывать файлы на запись и писать в них данные. Поскольку после 5 неудачных попыток войти в систему пользователь будет сразу получать отказ в доступе (без какой-либо записи данных в файл), надо найти 200 уникальных IP, с которых по пять раз и обратиться. Это возможно. Вешаем в баннерокрутилке html-баннер с пятью тегами:

<img src="http://user:password@www.host.ru/secret/absent.gif" width=1 height=1>


Пользователь моментально делает пять обращений сервер пять раз пишет в файл (кстати, в некоторых броузерах, возможно, выскочит окно для ввода логина и пароля). Можно сделать html-страницу с пятью такими картинками, а саму страницу вставить через iframe на посещаемый сайт (через iframe - чтобы по полю referer не нашли. Вряд ли служба поддержки халявного хостинга будет заниматься такими вещами как копание в лог-файлах в поисках рефереров). Те примеры, которые я привёл, разумеется, натянуты, но сам факт того, что можно воспользоваться таким недостатком системы, доказан. Кстати, нечто подобное уже было.

Но всё-таки приведу этот способ - зря писал, что ли? Его, кстати, можно без особого страха применять для ограниченного количества адресов (например, для локальной сети фирмы), положив в директорию файл .htaccess такого содержания:

order deny,allow



deny from all

allow from xxx.xxx.xxx

А вот код программы:

$errors = 0;

$fn = "ignore/". preg_replace("[^\d\.]", "", $REMOTE_ADDR. ".". $HTTP_FORWARDED_FOR);

if (is_file($fn)) {

  if (filectime($fn) < time()-3600)

    unlink($fn);

  else

    $errors = fread(fopen($fn, "r"), 2);

  };

if ($errors>5) {

  print ("Доступ закрыт. Зайдите через час.");

  exit();

  };

// здесь происходит установка связи с сервером БД. чтобы не трогать зря, если пользователя сразу же "отлупили".

$result = mysql_query("SELECT * FROM user WHERE login='". preg_replace("/[^\w_\-]/", "", $PHP_AUTH_USER). "' AND pass='". md5($PHP_AUTH_PW). "'");

if (@mysql_num_rows($result)!=1) {

  header("WWW-Authenticate: Basic realm=\"secret area\"");

  header("HTTP/1.0 401 Unauthorized");

  print ("Authorization required");

  fwrite(fopen($fn, "w"), ++$errors);



  exit();

  };

$current_user = mysql_fetch_array($result);

mysql_free_result($result);

Впрочем, грех работать с файлами, если есть база. Шутка.

Для непрошедших авторизаций создаём таблицу:

CREATE TABLE unauth (username VARCHAR(64) NOT NULL, pass VARCHAR(64) NOT NULL, ip VARCHAR(255), logintime TIMESTAMP)

И вместо обращения к файлам работаем с базой.

$errors = @mysql_result(mysql_query("SELECT count(username) as falses FROM unauth WHERE logintime>DATE_SUB(NOW(),INTERVAL 1 HOUR) AND ip='$REMOTE_ADDR $HTTP_X_FORWARDED_FOR'"),0);

if (mysql_error())

die(mysql_error());

if ($errors>5) {

  print ("Доступ закрыт. Зайдите через час.");

  exit();

  };

$result = mysql_query("SELECT * FROM user WHERE login='". preg_replace("/[^\w_\-]/", "", $PHP_AUTH_USER). "' AND pass='". md5($PHP_AUTH_PW). "'");

if (@mysql_num_rows($result)!=1) {

  header("WWW-Authenticate: Basic realm=\"secret area\"");

  header("HTTP/1.0 401 Unauthorized");

  print ("Authorization required");

  mysql_query("INSERT INTO unauth (username, pass, ip) VALUES ('$PHP_AUTH_USER', '$PHP_AUTH_PW', '$REMOTE_ADDR $HTTP_X_FORWARDED_FOR')");

  exit();

  };

$current_user = mysql_fetch_array($result);

mysql_free_result($result);

Хранить ли старые записи для статистики или нет - дело хозяйское. Если что, их можно удалять, выполняя перед авторизацией запрос:

DELETE FROM unauth WHERE logintime<DATE_SUB(NOW(),INTERVAL 1 HOUR)

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

На этом позвольте попрощаться.

P.S. Хотел начать писать про механизм сессий, но меня опередил DiMA.


Содержание раздела