Профессиональная разработка Web-приложений.  
Боишься нашего дизайна?
Новости
PDF журнал
Участники проектa
Сотрудничество
Ссылки
Карта сайта
Комментарии
Комментарии к статье
Добавить комментарий
Обсудить на форуме
Информация об авторе
Оценка статьи

Пароль на страницу

Простейшие способы закрыть директорию или файлы паролем.

Пароль на страницу. Часть 1. Скорее теоретическая.

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

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

Добавлю две вещи. Первое — это куда класть файл .htpasswd. Экспериментальным путем я выяснил, что если, например, путь к документу с сообщением об ошибке (ErrorDocument) пишется относительно системной переменной DocumentRoot. Но путь к файлу с паролями (UserFile) пишется относительно ServerRoot. Насколько я понял, выше ServerRoot положить .htpasswd нельзя — "../" не воспринимается. Всё это сделано для того, чтобы можно было поместить файл с паролями, например, одним уровнем выше корневой директории сайта, чтобы из сети доступа к файлу не было вообще.

Второе — это то, что скрипт может узнать, кто его открывает и пароль: переменные $PHP_AUTH_USER и $PHP_AUTH_PW.

Главный недостаток этого способа — сервер не может блокировать подбор пароля (это после нескольких неудачных попыток входа пользователю предлагается подождать часок-другой, а в течение этого времени обращения с его IP-адреса игнорируются). Это написано в официальной документации по Апачу.

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

Автоматизация авторизации

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

Каждая страница закрытой территории подключает файл с вот таким кодом:

$result = mysql_query("
 SELECT * FROM person 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=\"User area\"");
  header("HTTP/1.0 401 Unauthorized");
  print("Чтобы войти в пользовательскую часть сайта, надо ввести имя и пароль.");
  exit();
  };

$user_row = mysql_fetch_array($result);

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

Конечно же, пример, который я привёл, имеет ряд существенных недостатков. Не переписывайте его один-в-один, чтобы потом не пасть жертвой попыток подбора пароля, потому что
1. защиты от подбора здесь нет
2. если таблица пользователей большая, при подборе пароля злоумышленник, скорее всего, "завалит" базу

И последний на сегодня способ — хранение зашифрованных данных в куках.

Есть скрипт для входа, остальные подключают код, позволяющий только продолжить действия в закрытой области — если куки истекут, или он выйдет оттуда, придётся возвращаться на страницу для входа.

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

Все остальные программы подключают код, который делает следующее. Делает запрос в базу — выбирает строку с полученным логином. Из этой строки берет поле "log_time" и пароль и делает из них, как и описано выше, хэш. Сравнивает его с тем, что получил, и если они совпадают, выдает новую куку хэша, опять же, от пароля, времени и буквы "Ы" и делает запрос в базу данных "UPDATE user SET log_time='...' WHERE login='$cookie_login'".

if (isset($HTTP_COOKIE_VARS[$cookie_login]) && isset($HTTP_COOKIE_VARS[$cookie_code])) {

$login = $HTTP_COOKIE_VARS[$cookie_login];
$code  = $HTTP_COOKIE_VARS[$cookie_code];

$result = mysql_query("SELECT date_format(log_date,'%Y%m%d%H%i%s') as log_date1,pass,uid
FROM user WHERE email='$login' AND log_date>'DATE_SUB(NOW(),INTERVAL 15 MINUTE)'");
if (!mysql_error() && @mysql_num_rows($result)==1) {
  $log_time0 = time();
  $log_time1 = date("YmdHis", $log_time0);
  $log_time2 = date("Y-m-d H:i:s", $log_time0);
  $current_user = mysql_fetch_array($result);
  if (md5($current_user["pass"].$current_user["log_date1"].$md5letter) == $code) {
    mysql_query("UPDATE user SET log_date='$log_time2' WHERE uid=".$current_user["uid"]);
    setcookie($cookie_code, md5($current_user["pass"].$log_time1.$md5letter), 
      time()+900, $site_path);
    $auth = true;
    }
  else
    unset($current_user);
  };
};

Опять же, здесь нет никакой защиты от подбора и атаки на сервер (кстати, здесь можно вместо буквы "Ы" писать IP-адрес пользователя — чтобы, например, соседу по офису нельзя было взять файл с кукой и зайти со своего компьютера).

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

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

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

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

  • забывчивость (на это на приличных сайтах есть формочка "забыл пароль", чтобы отправить на введёный в системных настройках email этот самый пароль)
  • баловство ("ибо нефиг")
  • подбор пароля по словарю (вероятность удачного подбора велика, поэтому закрывать надо, тем более, если сайт коммерческого характера)
  • 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'"),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)

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

    Пароль на страницу. Часть 3. Пароль от базы

    Была у меня в своё время проблема: надо закрыть администрационную часть сайта, но при этом я не могу положить файл .htpasswd выше корневой директории сайта. Врождённая подозрительность не позволяла положить файл с паролем и отдельную директорию и заблокировать доступ к ней по http. Решил попробовать сделать защиту как в phpMyAdmin: у пользователя спрашиваются логин и пароль, с которыми скрипт соединяется с базой. В своём анализаторе логов я сделал именно так. Удобство метода в том, что файл можно складывать куда угодно — никаких кук, никаких директив сервера для директории. Заодно, если поменяется пароль в базе данных, не надо ничего исправлять в скрипте.

    Распишу метод на примере MySQL. Пишем функцию, например, mysql_die:

    function mysql_die() {
      header("HTTP/1.0 401 Unauthorized");
      header("WWW-authenticate: basic realm=\"Statistics\"");
      print ("Access denied. User name and password required.");
      exit();
    }

    В начале программы указываются хост сервера БД и, если надо, имя базы:

    $db_host = "localhost";
    $db_name = "somedatabase";

    А для соединения с базой берутся переменные сервера: $PHP_AUTH_USER и $PHP_AUTH_PW.

    $db_connect = @mysql_connect($db_host, $PHP_AUH_USER, $PHP_AUTH_PW)
      or mysql_die();

    И всё. Теперь о недостатках. Разумеется, с такой защитой можно пробовать подбирать пароль (в принципе, можно приделать блокировку, но тогда потерятеся вся красота метода). Пароль, как и в случае защитой средствами сервера, пересылается в открытом виде. Но для простых задач такое вполне сгодится.

    На днях в форуме разгорелась дискуссия, что лучше где применять — защиту через 401-й код, свои куки или сессии. Я не стал ввязываться, чтобы не растерять красноречия, но в ближайших выпусках опишу в общих чертах последние два метода (а пока рекомендую читать статью про сессии).




  • For comment register here
       2001-03-21 15:20
    Привет! Это конечно интересно, но ни чего нового ;(. Я вот хочу спросить, может Вы знаете, можно ли авторизировать юзера через свою форму если директория закрыта .htaccess, то есть в скрипте подставить данные в переменные $PHP_AUTH_... Подобные вопросы встречал на многих буржуйских и наших форумах, но ни где небыло ответов.

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

       Unknown 2001-03-21 16:13
    Вопрос, можно ли так авторизовывать, слышал, ответа - нет. Про лисапеды знаю и честно написал об этом. Про ".ht" тоже знаю.

       2001-03-28 10:30
    С нетерпением жду продолжения! ...а каким образом буква "Ы" усложняет подбор хеша?

       Unknown 2001-03-28 11:23
    Её можно заменить в любой момент. И подборщик не будет знать, что там. Кстати, вместо просто одной буквы можно легко написать бессмысленну комбинацию из 10-20 букв/цифр. И такую фиг кто подберёт. Врочем, это не панацея защиты.

       2001-03-28 14:36
    Re:WEBoy

    Дело в том, что если ты Дело в том, что файлы .htaccess и форма не совместимы, по двум простым причинам

    1. переменные $PHP_AUTH_USER,$PHP_AUTH_PW нельзя поменять из скрипта.

    2. чтобы их установить надо послать юзеру BasicRealm, что ессно не есть форма.

    Так что, или так или так..

    Удачи.

       2001-04-04 19:39
    Ребяяата!!! Я стока всего знаю, ооой... ик
    А Вы Знаете? Представляете!!! , оказывается можно ик...
    В общем представьте себе, что на самом деле можно из ик...
    Ну из этого..., как его? А!!! PHP4, обращаться к Муське, во! ик...
    А еще говорят..., неее, я точно знаю ик..., просто не совсем уверен ик...
    Что из PHP3 тоже можно ик.... Я правда еще точно не знаю, кхм
    Кто такая Муська и как именно к ней обращаться, но попробую спросить у одного ик... Товарища, вот, ну так вот, о чем это я? А! Я еще знаю!!! Ой, уже забыл ик...
    ик... ик... совсем забыл, ик... Я обязательно вспомню! И напишу!
    Если дадут... еще раз, ну написать, вот
    <+ RT % _ _ є +° ~$>

       2001-04-10 10:08
    to DJ:
    блин, не удобно отзывы читать, когда они в обратном порядке... может лучше будет ORDER BY date ASC ? =))
    пока не добрался до последнего, так и не понял, о чем писал Vitaliy =(
    Кстати, есть предложение:
    в хеадере форума поставить ссылочку на detail - для того чтобы человек, разобравшийся в чем-то мог написать статейку на detail.
    Между прочим, если раньше баннер висел, то теперь фиг найдешь хоть ссылку на detail с форума...
    С уважением.

       2001-04-25 10:39
    ИМХО, вместо буквы Ы использовать IP посетителя (вариант forvarded_for, если есть). И подбор затрудняется, и при заходе с др. адреса кого-то с таким хешем в то же время - отлуп.
    (У ИЕ есть такая бага - можно получть куку с известным именем даже (!!!) на другом сервере (не на том, кто её дал.))

       2001-04-28 05:36
    Господа, а как организовать авторизацию, если сервер не Апач, а IIS. Запрос на авторизацию функцией header не работает, $PHP_AUTH_USER и PHP_AUTH_PW следовательно тоже. Куки не проканают, т.к. не у всех юзеров они принимаются.

       2001-05-16 10:08
    ...это, конечно метод, но как тогда быть с корпоративными юзерами, сидящими в маскарадных сетках?
    у них же у вес один IP, хоть даже если их 100, и поллучается, что если среди них затесался
    хакерюга и лезет подбирать пароль к тебе на сервер, то после н-ного кол-ва попыток, он
    просто напросто отрубит всю свою сетку от доступа к твоему серверу...
    я думаю, что здесь нет единого оптимального решения.
    того, кто серьезно задумал поломать твой сервис остановить почти невозможно.
    для пользователей не-вебовских интерфейсов доступа к сервисам есть одни методы
    контроля.. для пользователей именно вэбовских интерфейсов - другие.
    я думаю, что розумнее всего контролировать вэбовского юзера с использованием
    сессий... в таком случае мы имеем дело с действительно уникальным параметром
    - ID сессии... ограничить число попыток входа в систему в рамках одной сессии
    это очень простая задача, но расчитывать на то, что хакер будет сидеть за окном
    броузера и постить тебе формы с подбором пароля - просто смешно.. скорее всего
    это будет специальный софт, который без проблем начнет новую сессию, когда ты
    закроешь предудущую... ты с ним в этом случае ничего не сделаешь.
    есть один выход - анализ трафика. Что я имею ввиду - а вот что:
    необходимо контролировать и анализировать общее количество сессий открытых
    в системе с определенного IP, причем не всех сессий а только тех
    которые были закрыты по причине ошибок ввода пароля.
    необходимо либо на угад, либо опытным путем установить критическое число для
    этого случая и только при достижении его, закрывать доступ этому IP в систему.
    Анализируя трафик, нужно не забывать о распределении отказов доступа во
    определенном временном промежутке, чтобы не перепутать обычный всплеск пользовательской
    активности с атакой хакера... короче здесь еще говорить и говорить.
    вот, такая получилась маленькая статья. Я, кстати, сейчас работаю именно над
    такой системой, если есть желание, можем обсудить возникающие вопросы - если это еще актуально.

    ------------------------------
    mailto:army@mail.od.ua
    KOSTYSH

       Unknown 2001-05-20 06:01
    никто не мешает юзеру (подбирающему пароль) выбирать каждый раз произвольный прокси, так что ip могут быть все время разные.
    мое мнение - если человеку очень надо поподбирать пароль - то никак ему не помешать (ни проверкой ip, ни сессиями, ни куками)
    поправьте меня если я неправ.

       2001-06-06 10:48
    А что такое HTTP_X_FORWARDED_FOR и с чем его едят, у меня в мануале этого нет, подскажите где это есть.

       Unknown 2001-06-06 11:41
    $HTTP_X_FORWARDED_FOR. Если прокси-сервер или файрволл не анонимный, он сообщает, на какой ip от него пошло соединение (обычно несколько ip через запятую).

       Unknown 2001-06-20 11:03
    А про flock() все забыли... :(((

       Unknown 2001-06-20 11:56
    Да почему же. Просто там хоть блокируй, хоть не блокируй, много попыток записи в один момент не есть хорошо.

       2002-04-22 20:55
    Точно, flock здесь следовало бы и упоминуть как альтернативный способ...
    Но система хитрая.

       2003-11-17 02:40
    Ссылки по теме устарели.

       2004-02-23 11:21
    aaa dfgdfg dfg dfg dfg ! Это конечно интересно, но ни чего нового ;(. Я вот хочу спросить, может Вы знаете, можно ли авторизировать юзера через свою форму если директория закрыта .htaccess, то есть в скрипте подставить данные в переменные $PHP_AUTH_... Подобные вопросы встречал на многих буржуйских и наших фор

       2004-10-20 18:35
    .htpasswd выше ServerRoot положить можно, если указывать абсолютный, а не относительный путь.
    Т.е. можно указать путь вида /usr/local/etc/apache2/.htpasswd

       2004-12-08 12:07
    s/авторизация/аутентификация/g

       2005-01-30 01:06
    Следующее замечание по поводу подбора пароля.
    Автор приводит пример: некто вешает на банерокрутилки обращение к нашему сайте с подстановкой значений. Мол 5 раз тыкнулись и система отшила "подборщика". Но баннер будет продолжать тыкаться на сайт. По истечении часа (не важно на сколько мы пользователя заблокировали) баннеры снова примуться за свое. Это ведь программа, ей все равно. Если бы это был человек, он бы пару часов посидел, поигрался и бросил подобное занятие. Получается, основная идея - борьба с программой подбора пароля.
    Но тут сразу приходит стандартное решение задачи: как отличить пользователя от программы? Поставить картинку с набором символов, созданную случайным образом. Если введены: логин, пароль и этот код - тогда мы уверены, что это человек и действуем в порядке вещей, если же код не введен - отшиваем сразу и не пытаемся даже соединиться с базой или производить каких-либо действий.
    Почему при регистрации нового пользователя данная технология используется, а для процесса аутентификации нет?

       2005-05-05 12:50
    А если блокировать имя в базе? Пароли ведь к именам подбираются. Даже не на час, а на минуту. Тогда брутфорс, например для admin станет слегка утомительным. Добавить в переменные "часто забываемые логины" и не трогать базу..

    Простейшие способы закрыть директорию или файлы паролем. пароль, htaccess, защита, авторизация, пример, безопасность

     
     
     
        © 1997-2008 PHPClubTeam
    []