Приемы сетевой обороны на PHP

Автор: content Понедельник, Апрель 9th, 2012 Нет комментариев

Рубрика: Язык PHP

Можно долго спорить, почему некоторым людям так нравиться гадить и ломать плоды трудов других людей, но так или иначе это факт, актуальный и для виртуального мира. Еще на заре зарождения домашних ПК, т.е. когда доступ к ним начали получать все желающие, началась эта чума. Был написан первый вирус, впервые взломан веб-узел… сейчас таким уже никого не удивишь. Многие уже привыкли, время от времени видеть надписи типа «Тут был я супер-пупер хакер» и другие проявления компьютерного вандализма.

Большинство таких атак происходят в результате использования «дыр» в серверных скриптах. Именно о прикрытии этих самых лазеек для хакеров и будет рассказано в данной статье.

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

Самой распространенной серверной технологией на сегодня является PHP. Думаю, раз вы читаете данную статью, не стоит снова останавливаться на том, что это такое, тем более на эту тему уже было немало хороших статей. Что ж, преступим.

Прежде всего, хочу сказать, что представленные тут примеры не гарантируют на 100% того, что вас никто не взломает, такое просто невозможно. Всегда, даже в самых распространенных и совершенных системах есть узкие места, пример тому сенсация полугодовой давности, когда на сайтах по всему миру в запросах на всеобще признанном языке SQL (Structured Query Language — структурированный язык запросов) была найдена грубейшая ошибка, получившая название SQL Injection. Но при этом вы увидите самые частые фатальные ошибки в защите и сможете на должном уровне защитить себя от атаки не только любителя, но и профессионального хакера.

Для начала вам нужно запомнить только одно — никогда не верить данным, полученным от посетителя — ведь они могут представлять собой вредоносный скрипт, который к стати, может быть как серверным, так и клиентским (будет работать на машинах посетителей). И этого вполне хватит, конечно, если в самих скриптах недопущены критические ошибки и нормально настроен сам сервер, но обо всем по порядку.

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

Для начала было бы неплохо ограничить длину имени и адреса e-mail. Это не только один из многочисленных методов частичной защиты, но и предохранение от шутников, которые думают что имя длиной в несколько сот символов очень забавно. Так что давайте в поле для ввода напишем, скажем, maxlength=25, например:

<input type=text name=user_email maxlength=25>

Таким способом никто не сможет ввести в данное поле более 25 символов. Однако это остановит только виртуальных вандалов-новичков ведь в адресной строке запросто можно написать что-то типа:

…guest.php?user_email=ha_ha_ha_slabaja_zashita_ha_ha_ha_tyt_bil_super_haker…

Что же, нанесем второй удар, написав в самом начале PHP скрипта примерно такое:

<?php

$user_email=$_POST['user_email'];

Т.е. значение переменной $user_email берем прямо из полей соответствующих значений POST массива. И так для каждой переменной. К стати, в таком случае нужно подправить и форму для отправки сообщений, явно указав метод передачи данных — method=»post», например:

<form action=»guest.php» method=»post»>

Сможет ли теперь хоть что-то сделать хакер? Сможет, сможет и несомнивайтесь.

Напомню, что при передаче данных на сервер при использовании метода POST, в отличие от метода по умолчанию — GET, данные передаются не через адресную строку в броузере, а вместе с пакетами данных, т.е. и POST можно подделать на чем угодно — начиная от стандартных программ из поставки Windows и заканчивая тем же Делфи — отослать запрос вида:

POST /guest.php HTTP/1.0

user_email=vetaki_ja_tebla_vzlomal…

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

Так что же делать? Паниковать! Нет, конечно же, шучу.

Если хакер попытается вот так нагло передать данные, мы можем остановить его следующим образом:

<?php

$referer=getenv(«HTTP_REFERER»);

if (!ereg(«^http://my.domain.com»))

{

echo «Hack off»;

exit;

}

Как видите, мы проверяем, послан ли запрос с одной из наших страниц, открытых в броузере (наш домен — http://my.domain.com) если все верно — выполняем что надо, ну а если нет- выводим поздравление хакеру: «Hack off» и заканчиваем работу скрипта: exit;.

Ну что, уже, ликуете? А зря. Напомню что переменная HTTP_REFERER формируется броузером посетителя, т.е. на стороне клиента (читать — хакера), а из всего этого получается, что и в ее подлинности мы не можем быть уверены. Подделать ее так же несложно, как и POST запрос.

Уже устали и не верите в свои силы против этих всемогущих хакеров? Не стоит. Враг хоть и не выдает себя, но уже окончательно устал, до данного этапа дойдут в лучшем случае 5-10% всех пытающихся. Так что, не будем разворачиваться у самого финиша, нанесем сокрушительный удар.

Всегда все верно говорят, что со стороны хакеров любая система имеет уязвимости. Но я еще ни разу почему-то не слышал чтобы говорили про обратное — ведь у любой системы есть места, где хакер уже беспомощен. Самое время применить специальное вооружение админов. А именно безысходность выполнения PHP сценария: как бы хакер не изощрялся, если стоит exit; — значит exit и точка.

Итак, допустим нам попался такой редкий хакер, что прорвался сквозь всю нашу предыдущую линию обороны и может спокойно посылать серверу переменные любой нужной длины, откуда хочет. Ну и пусть, а мы добавим сразу после проверки HTTP_REFERER и присвоения значения из массива (например, с POST) жесткую урезку строки:

$user_email=substr($user_email,0,25);

Теперь мы имеем переменную $user_email длиной в 25 символов (если ее исходная была больше, остальные ее символы были отброшены) и не один хакер не в силах этого поменять.

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

Какие символы следует блокировать? Это зависит от поля, например, в имени это могут быть все, кроме букв из алфавита, пробела, цифр, ну и пусть знака _ т.е. нам следует поступить, например, так:

if (preg_match(«/[^(\w)|(\x7F-\xFF)|(\s)]/»,$user_name))

{

echo «В имени есть запрещенные символы…»;

exit;

}

Теперь имена в гостевой будут чистенькими и ровненькими.

Для адреса электронной почты следует разрешить собаку @ и точку, а пробелы и русские буквы запретить.

Для тела сообщения также < следует менять на < а > на > например, так:

$message=ereg_replace(«<»,»<»,$message);

$message=ereg_replace(«>»,»>»,$message);

А знаки переноса строки на тег переноса <br>:

$message=ereg_replace(«(\r\n|\n|\r)»,»<br>»,$message);

Можно использовать и специальные функции PHP — htmlspecialchars(), nl2br() и другие, в общем, думаю, сами разберетесь, а то от темы уходим (а если не разберетесь — мыльте).

Также может быть уместна проверка на пустые сообщение или имя пользователя. Ее можно осуществить как стандартной функцией empty() так и просто проверив, не равна ли переменная «» например:

if (empty($message))

{

echo «Пустое сообщение оставлять не стоит»;

exit;

}

или вот так:

if ($message==»")

{

echo «Пустое сообщение оставлять не стоит»;

exit;

}

В общем, дело вкуса.

Также, можно не просто выводить такие сообщения типа «Пустое сообщение оставлять не стоит» или «Hack off» можно и сохранять в файле на сервере/пересылать себе на почту IP адрес хулигана, явно получить его можно так:

$RIP=$GLOBALS['REMOTE_ADDR'];

Таким способом будет возможность и вычислить неудачника-взломщика или, скажем, ограничить попытки, не давая в сутки более, например 2 раз оставлять сообщения в гостевой книге. Но я бы не стал так делать. Причин много — начиная от прокси серверов (хотя можно без проблем обойти их и узнать реальный IP) и заканчивая тем, что диал-ап до сих пор господствует на просторах СНГ. К тому же вышеперечисленных мер защиты вполне достаточно, причем не только для гостевой книги.

Еще хочу обратить ваше внимание на честных пользователей, а ведь их более 99.99% из общего количества посетителей и из-за каких-то хакеров они недолжны страдать. О чем я говорю? Например, человек не знал, что мы запретили некоторые символы в имени и написал в поле имени что-то типа [NickName], а далее очень-очень долго писал слова благодарности или еще чего и в результате мы его грубо остановили, сказав, что имя не годится, возвращайся и делай все заново. Как вы думаете, он вернется? Возможно и вернется, но уже совсем с другими словами.

Что же делать? Писать возле каждой строки для ввода, что можно вводить, а что нет? Несерьезно. Можно, например, вместо блокировки таких имен, просто удалять заблокированные символы. Но я не думаю что и это лучший путь.

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

<script language=»JavaScript»>

function checK(f) {

if (f.email.value==») { alert(«Укажите адрес почты.»); f.email.focus(); return false }

if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,4})+$/.test(f.email.value)) { return true }

alert(‘Неверный адрес почты.\nПопробуйте еще раз.’); f.email.select()

return false

}

</script>

<form name=f action=»guest.php» method=»post» onSubmit=»return checK(this)»>

<input type=»text» name=»email» value=»введите ваш e-mail» onfocus=»if (this.select) this.select()» onclick=»if (this.select) this.select()» size=28>

<input type=»submit» name=»subscribe» value=»Ок»>

</form>

Как вы видите, после клика на кнопку Ок, данные, перед передачей скрипту guest.php, проверяются функцией checK, если введенный адрес пуст или содержит запрещенные знаки, пользователь получит сообщение: «Укажите адрес почты.» или «Неверный адрес почты.\nПопробуйте еще раз.» соответственно (\n — перенос строки) при этом обратите внимание, что сообщения будут в окне: alert() и никакой перезагрузки страницы даже и не произойдет: return false , а курсор выделит ошибочный ввод: f.email.focus(); или же f.email.select() что очень удобно для пользователя, особенно если на странице поле для ввода не одно.

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

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

Итак, что же я там нашел? В принципе, ничего особенного за исключением того, что в некоторых, в том числе и «секретных» директориях отсутствовали файлы index.html (или другие index.*, интерпретируемые как стартовые). Кроме того, не было соответствующих настроек прав доступа. Что из этого следует? При наборе в адресной строке такого адреса на директорию без index.* файла, злоумышленнику прямо в браузере откроется все содержимое папки со всеми возможными последствиями (это уже в зависимости от того, что там храниться).

Как этого не допустить? Достаточно в каждую папку на сервере, если там нету файла index.*, поместить его туда. В самом файле можете писать, что вам угодно — от «Вход запрещен» или пустого файла до перенаправления, например на стартовую страницу сайта. Второй вариант предпочтительнее опять же с заботливого взгляда вебмастера в сторону добросовестных посетителей. Если кто подзабыл, напомню, как осуществить ридерект:

<html>

<head>

<META HTTP-EQUIV=»Refresh» CONTENT=»0; URL=http://syte.com»>

</head>

<body></body>

</html>

Ну, конечно еще оформить можно по своему усмотрению и т.д. и т.п.

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

Мы небудем рассматривать все свойства данного файла конфигурации, так как нас интересует только одно — защита файловой системы сервера (для тех, у кого жажда познаний неиссекаема, специально нашел неплохое описание данного файла настроек на русском языке: http://www.kokos.ru/?nma=catalog&fla=stat&cat_id=8&page=1&num=16 ).

Для запрета или разрешения доступа нужно использовать директивы Deny или Allow соответственно. Перед данными директивами идет директива Order, указывающая порядок следования директив Deny и Allow. Для лучшего понимания, давайте рассмотрим пример:

Order Deny,Allow

Deny from all

Allow from syte.com

Allow from 10.25.0.55

Поместив данные директивы в файл .htaccess, мы запретим доступ ко всем ресурсам в этой и сложеных папках всем, кроме компьютеров с адресами syte.com и 10.25.0.55.

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

Сессии в PHP

Нет, студенты, не пугайтесь, сессии в PHP намного более приятные процедуры, чем привычные для вас, зимние или лентие.

Когда были выпущены первые версии PHP, программисты столкнулись с серьезной проблемой, а именно с отсутствием глобальных переменных — т.е. все результаты работы скрипта, хранимые в переменных, после его исполнения уничтожались и небыли доступны в дальнейшем (наподобие того как в Паскале и других языках программирования, после завершения работы функции, уничтожаются переменные, объявленные внутри этой функции). Легче всего для понимания примеры, что ж, давайте, рассмотрим пример:

Пусть файл index.php содержит:

<?php

$test=»Этот текст задается в файле index.php»;

echo $test;

?>

И есть файл test.php, содержащий следующий код:

<?php

echo $test;

?>

Если выполнить данные скрипты, то в результате работы первого скрипта мы получим надпись «Этот текст задается в файле index.php». Второй же скрипт выдаст нам пустоту, так как значение переменной $test не передалось второму скрипту (и небыло в нём задано).

Тогда программисты и начали использовать Cookies для хранения глобальных переменных. Однако у этого метода есть большие недостатки. Начиная от громоздкости и заканчивая, наверное самым неприятным — все хранится на стороне пользователя (читать — хакера). Да и в конце концов, у пользователя может быть попросту отключены Cookies. Много программистов в те времена перестали использовать PHP.

Однако, появление сессий все изменило. Теперь вся информация хранилась на сервере, а идентификация пользователя проходила по уникальному идентификатору сесси. Но обовсем по порядку.

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

После начала сессии можно задавать глобальные переменные с помощью функции session_register(«var_name»). После этого переменная $var_name становиться доступной на всех страницах сессии. Давайте модифицируем представленный выше пример:

Файл index.php:

<?php

session_start();

$test=»Этот текст задается в файле index.php»;

session_register(«test»);

echo $test;

?>

Сессия запущена<br>

Теперь перейдем и посмотрим результат:<br>

<a href=»test.php»>работа сессии</a>

И файл test.php:

<?php

session_start();

echo $test;

?>

Открываем index.php, кликаем на ссылку и видим что открывшийся test.php получил значение переменной $test. Обратите внимение что в функции session_register(«test») имя переменной нужно передавать без знака $. Таким образом, после задания переменной $test как глобальной для сессии, она будет доступна во всех дальнейших скриптах данной сессии.

Если переменная больше непонадобиться, ее можно удалить функцией session_unregister();

Также можно уничтожить саму сессию: session_destroy();

Теперь у нас достаточно знаний чтобы написать механизм авторизации. Исполним его тремя файлами: index.php, auth.php и done.php. Файл index.php будет содержать форму для ввода логина и пароля. Данные из этой формы будут переданы для проверки файлу auth.php, который в случае удачной авторизации, даст пользователю доступ к файлу done.php.

Файл index.php:

<html>

<body>

<form action=»auth.php» method=»post»>

Логин <input type=»text» name=»user_name»><br>

Пароль <input type=»password» name=»user_pass»><br>

<input type=»submit» name=»submit» value=»вход»>

</body>

</html>

Файл auth.php:

<?php

session_start();

if ($submit)

{

if (($user_name==»login»)&&($user_pass==»password»))

{

$login_user=$user_name;

session_register(«login_user»);

header(«location: done.php»);

exit;

}

}

?>

<html>

<body>

Неверный логин или пароль

</body>

</html>

Тут, давайте разберемся с кодом. Итак, с начала мы открываем сессию: session_start(); далее проверяем были ли отправлены данные из формы: if ($submit) — это поможет избежать атаки на перебор примитивных брутфорсов (программ и скриптов переборщиков паролей). Проверяем введенные логин и пароль: if (($user_name==»login»)&&($user_pass==»password»)) в данном случае для простоты у нас только одна пара логин-пароль, в действительности же логина и пароли хранятся в файлах или базах данных, но я не стал приводить такой пример чтобы не усложнять понимание сути авторизации и ее потенциально опасных мест. Позже мы обязательно рассмотрим, как и где хранить логины и пароли. Итак, если были введены правильный логин и пароль, объявляем глобальную переменную $login_user и перенаправляем броузер на страницу done.php: header(«location: done.php»);

И файл done.php

<?php

session_start();

if (!isset($login_user))

{

header («location: index.php»);

exit;

}

?>

<html>

<body>

Вы залогинены под логином:

<?php

echo «$login_user»;

?>

</body>

</html>

С данным скриптом все уже намного понятнее. После открытия сессии, проверяем залогинен ли пользователь: if (!isset($login_user)) если да, то выводим сообщение в котором указываем его логин: Вы залогинены под логином: <php echo «$login_user»; ?>, в противном же случае, перенаправляем его на страницу ввода логина и пароля: header («location: index.php»);

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

А потенциально опасными являются следующие моменты:

- несмотря на наличие проверки того что, данные отправлены из формы, можно спокойно сымитировать это и попытаться перебрать пароль через скрипт auth.php

- скрипт done.php можно обмануть так: done.php?login_user=login

Для устранения первой уязвимости желательно проделать все что было описано в предыдущей статье, а именно — жесткий приме переменной только из массива POST, проверка $HTTP_REFERER, проверка и урезка переменной. Также, раз нам нужно защититься от многочисленных атак, можно записывать IP посетителя и, скажем после 3 неудачных попыток блокировать его на 15 минут. Однако я бы посоветовал не применять блокировку IP — ее можно элементарно обойти, а многие пользователи прокси серверов могут страдать из-за нее. Гораздо разумнее применить задержку авторизации. Т.е. непосредственно перед проверкой корректности логина и пароля делаем задержку, скажем на 1 секунду. Пользователи ее скорее всего даже не заметят, а вот у хакеров скорость перебора упадет ниже 1 комбинации в секунду, что фактически полностью исключает возможность перебора пароля даже по специальному словарю. Осуществить задержку можно так:

sleep(1); //задержка на 1 секунду

Что же касается второй проблемы с защитой, там все еще легче. Несмотря на то что любой желающий может передать переменную $login_user, содержащую произвольный логин скрипту done.php, все же кое-что можно сделать. А именно удалить переменную (в PHP нет нужды объявлять переменные, поэтому и понятие удаления переменной можно сравнить скорее с очисткой переменной) с помощью функции unset(); после чего откроем сессию, в которой хранится значение переменной $login_user, взятое с сервера, т.е. истинное значение, на которое хакер никак не может повлиять. Сделать это можно так:

<?php

unset($login_user);

session_start();

if (!isset($login_user))

Как видите, если переменная $login_user и была передана взломщиком скрипту, мы очищаем ее, а далее уже открываем сессию и если там содержится переменная $login_user — т.е. если была произведена успешная проверка логина и пароля то даем посетителю доступ к странице.

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

Вы еще тут? Так и знал, уже побежали за компы защищать свои творения и дорабатывать юзабилити. Если есть вопросы — смело пишите. До скорых встреч.

Источник: http://www.php.su/articles/?cat=security&page=010

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты