Базовая HTTP-авторизация – защита от честных людей
Базовая авторизация используется повсеместно для ограничения доступа к «личным кабинетам», «панелям управления», администраторским веб-интерфейсам, форумам и многим другим веб-ресурсам. Думаю, рядовым пользователям сети будет любопытно узнать, как работает это средство и насколько оно надёжно. Начинающим веб-мастерам будет интересно, как его подключить. А веб-программисты со стажем наверняка задавались вопросом, можно ли усилить защиту.
Basic Authorization под микроскопом
За работу механизма так называемой базовой авторизации (далее просто BA – Basic Authorization) на стороне сервера отвечает не какое-то специфическое ПО, а сам сервер.
Давайте рассмотрим диалог клиента и сервера при попытке получить доступ к конфиденциальной информации.
Когда пользователь впервые пытается получить защищённый документ, щёлкнув мышкой по ссылке, по кнопке в форме или просто набрав URL, браузер (клиент) посылает на сервер самый обычный запрос. Это неудивительно – браузер пока не знает, что доступ к этому документу ограничен. Заголовки HTTP-запроса могут выглядеть приблизительно так:
GET / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.7)
Gecko/20041016 Firefox/0.9.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;
q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.7,ru;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: KOI8-R,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
|
Но сервер возвращает не обычный ответ с кодом 200 (200 означает, что запрос обработан успешно, ответ отправлен), а сообщение о том, что для получения доступа требуется авторизоваться. Вот возможный набор заголовков ответа:
HTTP/1.1 401 Authorization Required
Date: Tue, 01 Mar 2005 11:30:10 GMT
Server: Apache/1.3.33 (Unix)
WWW-Authenticate: Basic realm="How about authorization?"
Connection: close
Content-Type: text/html; charset=iso-8859-1
|
Необычным в нём является статус (первая строка), который равен не 200, как при «нормальном» ответе, а 401. Также в нём имеется поле WWW-Authenticate, сообщающее браузеру детали: авторизация будет проходить по Basic-сценарию, пользователю рекомендуется сообщить указанную фразу.
Вслед за этими заголовками передаётся тело документа, которое браузер пока не отображает, а выдаёт диалоговое окно с просьбой ввести имя и пароль.
Если пользователь откажется от ввода пароля, нажав кнопку «Отмена», то браузер отображает тело полученного документа.
Очень широко распространено заблуждение, что в ответ на отказ от ввода пароля (или после ввода неверного имени/пароля) сервер высылает документ с сообщением об ошибке 401. Это не так! Сервер высылает сообщение 401 всегда, когда запрашивает пароль. Когда пользователь нажимает «Отмена», браузер вообще не обращается к серверу[1] – необходимый документ уже загружен, его осталось только показать пользователю.
Если пользователь ввёл имя и пароль, то сразу после нажатия кнопки «ОК» браузер отправляет эту информацию на сервер в новом запросе, заголовок которого будет примерно таким:
GET /paper/1.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.7)
Gecko/20041016 Firefox/0.9.3
Accept: text/xml,application/xml,application/xhtml+xml,text/html;
q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.7,ru;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: KOI8-R,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Authorization: Basic MTox
|
Как видите, это снова обычный GET-запрос, но теперь сервер получил информацию о пароле и имени пользователя в строке Authorization. Секретная информация не защищена, а просто закодирована методом base64 (RFC 2045). Если декодировать строку MTox, то вы получите имя и пароль, разделённые двоеточием. То есть никакой секретностью тут и не пахнет.
Если имя и пароль удовлетворят сервер, то пользователь получит требуемый документ. Набор заголовков ответа будет выглядеть как обычно:
HTTP/1.1 200 OK
Date: Tue, 01 Mar 2005 11:41:36 GMT
Server: Apache/1.3.33 (Unix)
Last-Modified: Tue, 01 Mar 2005 11:22:32 GMT
ETag: "4e598b-33-42245078"
Accept-Ranges: bytes
Content-Length: 51
Connection: close
Content-Type: text/html; charset=koi8-r
|
Если пара имя/пароль не верна, то сервер просто снова выдаст документ-запрос 401, повторно инициируя диалог браузера с пользователем.
После первой авторизации браузер запоминает имя и пароль и сообщает серверу эту информацию при всех последующих обращениях. Сервер больше не будет обрабатывать ошибку 401, а от пользователя не потребуется повторного ввода пароля. Процесс авторизации прошёл успешно, но обратите внимание на то, что в результате не была открыта сессия. Иллюзию непрерывной сессии создаёт браузер, который фактически авторизуется при каждом запросе. К этому существенному недостатку BA мы ещё вернёмся.
Кроме того, практически все современные браузеры оснащены менеджерами паролей и способны сохранять пароли на жёстком диске. Если ваш браузер запомнил ваш пароль неделю назад, то сегодня он может избавить вас и сервер от утомительной процедуры авторизации. Читатель, я думаю, осознаёт всю сомнительность такой «услуги» браузера, ведь всю неделю пароль был подвергнут серьёзной опасности.
Два слова о настройке Basic Authorization
«Минусы» BA рассмотрим чуть позже, а сперва оценим главный её «плюс» – простоту настройки.
Сегодня уже трудно найти host-провайдеров, у которых в списке предоставляемых возможностей не значилось бы «пароллирование директорий». Воспользоваться этой возможностью совсем несложно. Если вы захотели ограничить доступ к определённой директории (и всем вложенным в неё), достаточно разместить в ней файл .htacess примерно следующего содержания:
AuthName "How about authorization?"
AuthType Basic
Require valid-user
AuthUserFile /путь/к/файлу/.htpasswd
или добавить подобные команды в имеющийся .htaccess. Смысл и назначение этих директив достаточно очевидны. AuthName задаёт строку, которую браузер должен показать пользователю при запросе имени и пароля. AuthType задаёт тип авторизации (Basic). Директива Require способна выполнять различные проверки легальности доступа. Здесь вы видите её элементарное применение: мы потребовали, чтобы посетитель был зарегистрированным пользователем. Точнее, мы потребовали, чтобы посетитель получил доступ к файлам только в том случае, если он успешно прошёл процедуру авторизации. Директивой AuthUserFile указываем файл с паролями.
Файл с паролями .htpasswd создаётся и дополняется утилитой htpasswd, входящей в дистрибутив Apache. Располагать его безопаснее вне дерева каталогов, доступных по HTTP.
Я не сказал ещё про одну Auth-директиву – это Auth-GroupFile. С её помощью можно задать файл, описывающий группы пользователей. К сожалению, информация о группе пользователя может быть использована только в директиве Require. Поэтому разбиение пользователей на группы практически ничем не расширяет возможности администратора и используется редко.
Защитить паролем можно директорию и с HTML-документами, и с CGI-скриптами, и даже с графикой. Одним словом, абсолютно любую директорию, доступную через Web.
Есть ещё одна инструкция, к которой мы сегодня обратимся, хотя она и не относится напрямую к авторизации. Если добавить в .htaccess строку:
ErrorDocument 401 /путь/документ_или_сценарий
то указанный документ (или результат работы указанного сценария) будет высылаться с ответом 401. Пользователь, как вы помните, увидит этот документ, если откажется от авторизации.
Уязвимости Basic Authorization
Итак, BA страдает практически всеми возможными уязвимостями, какие только можно придумать.
Передача открытого пароля
Как вы видели, пароль и имя пользователя передаются нешифрованными (base64-кодирование никак нельзя назвать защитой). Более того, секретная информация оснащена весьма броской «меткой» – текстом «Authorization», которую легко найти в общем потоке данных. Кроме того, если злоумышленник не смог вычленить из трафика пароли с первой попытки, то ему будут предоставлены новые и новые возможности, ведь имя и пароль передаётся при каждом запросе. Вам остаётся только надеяться, что ваш трафик никто не анализирует. К счастью, большинство пользователей Интернета не имеет возможности просмотра вашего трафика.
Защита от перехвата
Можно ли защититься от перехвата? Нет! По крайней мере до тех пор, пока вы остаётесь в рамках протокола HTTP и механизма BA.
Возможность подбора пароля
Как видите, BA не предоставляет никаких средств, ограничивающих количество неудачных попыток авторизоваться. То есть злоумышленник может сколько угодно подбирать пароль. Хуже всего то, что перебором может заняться любой пользователь Интернета. Конечно, не факт, что он отгадает ваш пароль, но, когда одновременно подбор ведёт множество «агентов», опасность взлома, как вы понимаете, умножается, даже если шансы каждого будут невелики.
Защита от подбора
Этот недостаток можно частично скомпенсировать, и здесь нам поможет директива ErrorDocument. С её помощью можно назначить CGI-скрипт ответственным за обработку ошибки 401. Например:
ErrorDocument 401 /cgi-bin/401.cgi
Самая простая мера, которую можно реализовать таким образом, – это ведение протокола автризаций. Журнал не избавит вас от атак, но вы хотя бы будете знать о них и об их источнике. А знание – сила.
Конечно, вы можете возразить, что всю необходимую информацию можно собирать в log-файлы сервера. Это так. Пользуясь этими файлами, несложно обнаружить попытки подбора пароля, если у вас один посетитель в день, он один пытается ломать защиту, и вы просматриваете статистику каждый день. Если же в день ваш ресурс посещают сотни пользователей, а попытки взлома случаются раз в год, журналы (хуже! – лишь выжимки из них) вы просматриваете примерно с такой же периодичностью, то обнаружить злоумышленников довольно трудно. Кроме того, на многих хостингах у владельца нет возможности просматривать log-файлы или управлять их форматом. А используя CGI-сценарий, можно не только вести журнал, но и, скажем, формировать e-mail-сообщения администратору в «подозрительных» случаях.
Вот простой пример CGI-сценария, написанного на shell. Он ведёт простой протокол, отмечая в журнале время и имя пользователя при каждой попытке авторизоваться.
#!/bin/sh
echo $REMOTE_ADDR ${REMOTE_USER:-nouser} `date` >>401.log
cat <<'TEXT'
Content-Type: text/html
<html>
<head><title>401</title></head>
<body><h1>Auth. Req.</h1></body>
</html>
TEXT
Команда echo добавляет строку в журнал, а cat выдаёт на стандартный вывод минимальный заголовок и тело HTTP-ответа.
Этот элементарный пример я привёл здесь не столько, чтобы обогатить человечество ещё одним полезным CGI-сценарием, сколько для того, чтобы обсудить два важных аспекта.
Во-первых, обратите внимание на использование переменной REMOTE_USER. Если эта переменная определена, то конструкция ${REMOTE_USER:-nouser} эквивалентна значению $REMOTE_USER, в противном случае вся конструкция эквивалентна строке «nouser» (о работе с переменными в shell см. man 1 sh).
При первом обращении клиента к серверу, когда Authorization-информация ещё не передаётся браузером, переменная REMOTE_USER не будет определена. Но если пользователь ввёл неверные данные – попытка авторизации была, но потерпела неудачу – то наш сценарий будет вызван повторно, а в переменной REMOTE_USER будет находиться имя, под которым пользователь пытался авторизоваться (даже если учётной записи для такого пользователя вовсе не существует).
Это, кстати, делает возможным определить причину ошибки 401 и разделить случаи, когда пользователь пытается войти в систему впервые и когда он делает повторную попытку.
Таким образом, наш простой пример заносит в протокол не только информацию о попытках авторизоваться, но и имена, под которыми не удалось авторизоваться. Это позволяет легко заметить, что кто-то занимается перебором. Если пользователь успешно авторизовался с первого раза, то в протоколе останется только запись о «nouser». В «боевых условиях» такие записи не представляют большой ценности, но при отладке они могут быть весьма полезны.
Во-вторых (и это, конечно, недоработка), обратите внимание на то, что мы в сценарии не проанализировали причину его вызова. Кроме того, мы не позаботились о статусе, который возвращает наш сценарий. Если скрипт будет вызван в результате действия нашей директивы Error-Document, то код ответа сохранится без изменений – 401. Но никто не мешает запустить этот сценарий не как обработчик ошибки, а напрямую, просто по его непосредственному адресу (например, http://host/cgi-bin/401.cgi). Тогда клиенту будет возвращён тот же документ с обычным кодом 200. Обратите внимание, тот же сценарий, что возвращал ошибку-запрос с кодом 401, теперь вернул код 200. Это произошло потому, что сам сценарий никак не влияет на возвращаемый код, и сервер выставляет код ответа на своё усмотрение. Такие вызовы будут ошибочно зарегистрированы в протоколе.
Конечно, в реальных условиях наш сценарий должен был бы проанализировать обстоятельства вызова. При первом обращении, без сообщения Authorization-информации, скрипт получает в своё распоряжение обычный набор REDIRECT-переменных: REDIRECT_REQUEST_METHOD, REDIRECT_STATUS, REDIRECT_URL, говорящие о том, что скрипт вызван не напрямую. При повторном вызове, в случае провала предыдущей попытки авторизоваться, скрипт получает вдобавок к упомянутым ещё две переменные: AUTH_TYPE, которая, конечно, равна «Basic», и REMOTE_ USER с именем «неудачника».
При простом, непосредственном, вызове сценария все перечисленные переменные просто не будут созданы.
Приведу пример shell-скрипта, анализирующего эти ситуации:
#!/bin/sh
if [ ${REDIRECT_STATUS:-)} = 401 ]
then
echo $REMOTE_ADDR ${REMOTE_USER:-nouser} `date` >>401.log
if [ ${REMOTE_USER:-D} != D ]
then
mess='Что-то вы зачастили неудачно авторизоваться!'
else
mess='Ошибка! (первая)'
fi
else
mess='Так этот скрипт вызывать нельзя'
fi
echo "Content-Type: text/html
<html>
<head><title>$mess</title></head>
<body><h1>$mess</h1></body>
</html>"
Как видите, теперь мы различаем три случая:
- первая попытка авторизоваться;
- не первая попытка авторизоваться;
- вызов скрипта напрямую, вернее, вызов не для обработки ошибки 401.
Но давайте не будем увлекаться. Подобные проверки необходимы, забывать про них нельзя, но они элементарно организуются на любом языке программирования и не заслуживают пристального внимания. Во всех последующих примерах я, для компактности, не буду их делать, предполагая, что вы при необходимости добавите соответствующий код.
Приводя соображения о возможности подмены статуса, я хотел подвести вас к следующему вопросу: что произойдёт, если обработчик ошибки 401 вернёт не код 401?
Вот пример такого обработчика:
#!/bin/sh
cat <<'TEXT'
Status: 200
Content-Type: text/html
<html>
<head><title></title></head>
<body><h1>вы не авторизовались и
не авторизуетесь</h1></body>
</html>
TEXT
Озадачены? В общем-то, не произойдёт ничего неожиданного. Давайте проследим всю цепочку событий. Когда неавторизованный пользователь обратится к серверу, произойдёт ошибка 401. Следуя инструкции ErrorDocument, сервер вызовет наш сценарий, который подменит код 401 на код 200 и выдаст обычный документ. Браузер получит его и отобразит, оставаясь в полном неведении, что произошло на самом деле на сервере. Пользователю не удастся получить доступ к засекреченной области, но и диалога для ввода пароля он не получит. Обращаясь к любому документу, пользователь будет получать только результат работы вашего скрипта (который в свою очередь тоже может организовать перенаправление). Мы заблокировали для пользователя возможность авторизации.
Этот короткий скрипт может пока послужить только для весьма сомнительной защиты. Использовать его можно только как-нибудь так: сперва защитить директорию; потом авторизоваться, указав браузеру запомнить имя и пароль; и подключить в качестве обработчика ошибки 401 наш скрипт. Всё! Больше диалога для ввода пароля никто не увидит, и только вы сможете пользоваться ресурсом, так как вам вводить имя и пароль больше не потребуется (пока ваш браузер их помнит). Такой подход хоть и имеет право на существование, но смотрится диковато. Тем более что правильнее было бы сказать не «вы имеете доступ к ресурсу», а «любой человек, воспользовавшийся вашим «заговорённым» браузером, имеет доступ к ресурсу». Это, как вы понимаете, не одно и то же.
Тем не менее все высказанные идеи можно объединить и развить, придав им более «товарный вид».
Следующий сценарий также является обработчиком ошибки 401. Он разрешает авторизацию не чаще, чем раз в десять секунд. Он уже написан на Perl, и является гибридом двух предыдущих shell-скриптов.
#!/usr/bin/perl
use strict;
my $LOGFILE='401.log';
my $lastlog = $^T-(stat $LOGFILE)[9];
if ($lastlog > 10) {
my $log=$ENV{'REMOTE_ADDR'}.
($ENV{'REMOTE_USER'} or 'nouser').
localtime($^T)."\n";
$log.=join('', map {" $_ $ENV{$_}\n"} sort keys %ENV);
open F, '>>'.$LOGFILE or die;
print F $log;
close F;
# Этот документ будет выслан ещё до того, как пользователь ввёл пароль!
print <<'TEXT';
Content-Type: text/html
<html>
<head><title>Документ 401</title></head>
<body><h1>Доступ закрыт</h1>
<p>Можно было авторизоваться, но вы
допустили ошибку при наборе пароля
или имени. Теперь регистрация
заблокирована на 10 секунд.</p></body>
</html>
TEXT
} else {
# этот документ пользователь увидит:
# - и если не вовремя пришёл
# - и если ввёл неправильный пароль
# анализируйте $REMOTE_USER для разделения этих ситуаций
print <<'TEXT';
Status: 200
Content-Type: text/html
<html>
<head><title>Документ 200</title></head>
<body><h1>Доступ вообще закрыт</h1>
<p>Вы не можете авторизоваться вообще.
Подождите 10 секунд.</p>
</body>
</html>
TEXT
}
Как видите, этот сценарий тоже ведёт протокол. По дате последней модификации log-файла мы определяем, давно ли была последняя попытка авторизоваться.
Если последняя попытка была более десяти секунд назад, то в протокол заносится ещё одна запись, а пользователю выдаётся документ с кодом 401. То есть мы даём пользователю возможность вести имя и пароль. Если последняя авторизация была менее десяти секунд назад, то клиенту выдаётся код 200.
Вы, наверно, уже заметили, что у нашего сценария есть большой изъян. (Не говоря о том, что он не выполняет проверку причины запуска.) После того, как авторизовался один пользователь, на десять секунд право авторизации теряет и он, и все остальные пользователи. Избавиться от этого гораздо труднее, чем может показаться на первый взгляд, потому что нам приходится принимать решение о допустимости авторизации ещё до того, как мы получим имя пользователя. И, напротив, после того, как сервер получил имя пользователя, наш скрипт запускаться уже не будет (если имя и пароль верны).
Использовать в этой ситуации IP-адреса или cookie не только ненадёжно, но и сложно. Ненадёжно потому, что разные пользователи могут приходить c одного IP-адреса, а cookie могут быть отключены (что, конечно, можно проверить, но только ценой дополнительных усложнений) или, хуже того, фальсифицированы. А сложно потому, что обрабатывать эти адреса и cookie должно нечто, не связанное с обработкой ошибки 401, нечто, работающее с уже авторизованным пользователем. То есть вам придётся самостоятельно реализовать скрипт, модуль сервера, или иное средство, выдающее ответы 200 и сами документы. Но тогда это ваше средство должно проверять, авторизовался ли пользователь. Как видите, мы пришли к тому, что вам придётся реализовать всю (или практически всю) функциональность сервера. Но если платить такую цену, то не за базовую же авторизацию. Тогда уж лучше сделать что-нибудь понадёжнее.
К счастью, во многих случаях у ресурса есть только один администратор (пользователь). Тогда указанная некорректность не играет роли.
Но это ещё не беда. Настоящая проблема состоит в том, что выдача кода 200 – это, на самом деле, не совсем блокировка авторизации. Мы просто лишаем пользователя возможности ввести имя и пароль. Злоумышленник по-прежнему сможет практически беспрепятственно перебирать пароли, создавая запросы искусственно (не с помощью браузера) и варьируя информацию в поле Authorization. Конечно, ответы нашего сервера (особенно с кодом 200) могут сильно затруднить работу супостата, но вряд ли мы сможем поставить врагу действительно существенный заслон.
Мы снова получили защиту от честных людей, хотя и усовершенствованную.
Невозможность «разлогиниться»
Разработчик веб-ресурса никак не может заставить браузер забыть пароль и имя пользователя. За этим стоит тоже сама природа протокола HTTP: сервер не может заставить клиента выполнить какие-то действия. Сервер может только рекомендовать, но для BA и такой возможности не предусмотрено.
Может показаться, что существуют возможности заставить браузер забыть пароль или заменить его на новый, неправильный. Например, можно выслать ещё раз ответ 401, созданный искусственно, и дать пользователю возможность ввести что угодно, заставив браузер «забыть» подлинную информацию. Но ни к чему хорошему это не приведёт. Если пользователь уже авторизовался, то на любой ответ 401 браузер не обращается к пользователю и не пытается изменить пароль, а просто повторяет запрос, высылая прежние имя и пароль.
Резюме
Надеюсь, что теперь вам будет легче взвесить все «за» и «против», когда в следующий раз доведётся оценивать безопасность того или иного решения. Вы видите, что, несмотря на возможность многих косметических улучшений, фактически, ни одно из слабых мест BA закрыть не удаётся в принципе. Однако, принимая окончательное решение, будьте снисходительны. Помните, что BA – проверенный и надёжный (в известном смысле) механизм. Кроме того, его настройка очень проста. Не факт, что созданный вами собственный аппарат защиты не будет обладать изъянами, невзирая на все труды, которые придётся в него вложить. Одним словом, я назвал здесь много «против», но не забывайте и про «за» – простоту и безотказность. Два эти довода способны перевесить все «против», что подтверждается повсеместным использованием BA, невзирая ни на что.