Pers.narod.ru. PHP. Статьи. Защита форм от "доступа со стороны"

В этой статье я хотел бы обратить внимание на одну распространённую проблему, связанную с HTML-формами, без которых не обходится ни одно приложение PHP. Как правило, пользователь отправляет скрипту данные, введённые им в форму HTML. Теоретически ничего не мешает пользователю создать локальную копию страницы с формой, изменить URL-адрес её обработчика на абсолютный, если он таковым не был (поставить action="http://www.нашсайт.ru/script.php" вместо action="script.php" в теге <form>) и начать "бомбить" наш скрипт данными в ручном или автоматическом режиме, например, с целью подбора пароля или просто резкого увеличения нагрузки на сервер.

Как этого избежать? Один из классических путей состоит в разбиении скрипта на отдельные модули, главный из которых (обычно это файл с именем index.php) определяет некоторую константу, показывающую, что мы вошли в систему, а вызываемые им модули, содержащие функционал, проверяют наличие такой константы. Допустим, в файле index.php определена константа IN_SITE и вторая константа, ROOT_PATH, содержащая путь к папке с модулями:

<?php
 define('IN_SITE', true); //Константа, которая будет показывать, что мы внутри системы
 define('ROOT_PATH', './'); //Путь к папке, откуда будут включаться файлы с модулями
 
 //...(1)
 include(ROOT_PATH.'module.php'); //Подключаем модуль, он будет проверять наличие константы IN_SITE
?>

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

//--------- Блок дополнительной защиты
 //У нас PHP 5 и выше с отключённой директивой register_long_arrays?
 if (@phpversion() >= '5.0.0' && (!@ini_get('register_long_arrays') || @ini_get('register_long_arrays') == '0' || 
  strtolower(@ini_get('register_long_arrays')) == 'off')) {
  //Ставим суперглобальные массивы в их старые значения (совместимость с PHP ниже 4.1.0)
  $HTTP_POST_VARS = $_POST;
  $HTTP_GET_VARS = $_GET;
  $HTTP_SERVER_VARS = $_SERVER;
  $HTTP_COOKIE_VARS = $_COOKIE;
  $HTTP_ENV_VARS = $_ENV;
  $HTTP_POST_FILES = $_FILES;
  //Сессия - единственный суперглобальный массив, которого может не быть
  if (isset($_SESSION))	{ $HTTP_SESSION_VARS = $_SESSION; }
 }
 //Защита от взлома через GLOBALS
 if (isset($HTTP_POST_VARS['GLOBALS']) || isset($HTTP_POST_FILES['GLOBALS']) || 
  isset($HTTP_GET_VARS['GLOBALS']) || isset($HTTP_COOKIE_VARS['GLOBALS'])) {
  die('Hacking attempt detected!');
 }
 //Защита от взлома через HTTP_SESSION_VARS
 if (isset($HTTP_SESSION_VARS) && !is_array($HTTP_SESSION_VARS)) {
  die('Hacking attempt detected!');
 }
 
 if (@ini_get('register_globals') == '1' || strtolower(@ini_get('register_globals')) == 'on') {
  //Определяем "массив допустимых массивов"
  $not_unset = array ('HTTP_GET_VARS', 'HTTP_POST_VARS', 'HTTP_COOKIE_VARS', 'HTTP_SERVER_VARS', 
   'HTTP_SESSION_VARS', 'HTTP_ENV_VARS', 'HTTP_POST_FILES');
  //Если нет сессии - определить её массив
  if (!isset($HTTP_SESSION_VARS) || !is_array($HTTP_SESSION_VARS)) {
   $HTTP_SESSION_VARS = array();
  }
  //Объединяем суперглобалы в один большой массив
  $input = array_merge($HTTP_GET_VARS, $HTTP_POST_VARS, $HTTP_COOKIE_VARS, $HTTP_SERVER_VARS, 
   $HTTP_SESSION_VARS, $HTTP_ENV_VARS, $HTTP_POST_FILES);
  unset($input['input']);
  unset($input['not_unset']); //Для безопасности разопределяем одноимённые с массивами элементы
  while (list($var,) = @each($input)) { //Проверяем, нет ли "левых" данных
   if (in_array($var, $not_unset)) { die('Hacking attempt!');  }
   unset($$var);
  }
  unset($input); //Избавляемся от массива $input
 }
//--------- Конец блока дополнительной защиты

Недостаток приведённого кода - во всех модулях нужно писать $HTTP_POST_VARS вместо $_POST, как это делалось на заре развития PHP (до версии 4.1). С другой стороны, для совместимости со старыми версиями и модулями PHP это скорее достоинство.

Теперь любой модуль будет проверять наличие константы IN_SITE и не даст вызвать себя "напрямую":

<?php
 if (!defined('IN_SITE')) { die ('Hacking attempt detected!'); } //Если модуль вызван "напрямую" - завершить
 //...(2)
?>

Далее в коде модуля могут содержаться обычные действия по обработке параметров, выводу форм и т.п., например, поставим следующий код вместо комментария (2):

require_once 'functions.php';
 $params = array ('name','action');
 require_once ('params.php');
 if (!empty($action)) {
  echo "<p>Спасибо, $name, Ваши данные отправлены</p>";
 }
 else {
?>
 <form method="get">
 <table align="center" border="0" cellpadding="4" cellspacing="0" width="90%">
   <tr>
    <td>Имя:</td>
    <td>
     <input type="text" name="name" maxlength="40" size="40" value="<?php echo $name; ?>">
    </td>
   </tr>
   <tr>
    <td>&nbsp;</td>
    <td>
     <input type="submit" name="action" value="Отправить"> 
     <input type="reset" value="Отмена">
    </td>
   </tr>
  </table>
 </form>
<?php
 }

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

<?php
 while (list($num,$var) = each($params)) {
  if (!empty($_POST[$var])) $$var = trimall(htmlspecialchars(magic($_POST[$var])));
  else if (!empty($_GET[$var])) $$var = trimall(htmlspecialchars(magic($_GET[$var])));
  else $$var = '';
 }
?>
<?php
 function trimall ($string) { 
  $string=str_replace("\r","",$string);
  $string=preg_replace("/\n\n+/","\n",$string);
  return preg_replace("/  +/"," ",trim($string));
 }
 function magic($path) { //кавычки
  ini_set('magic_quotes_runtime', '0');
  ini_set('magic_quotes_sybase', '0');
  //В php.ini magic_quotes_gpc=1. Она не меняется программно - см. доки
  if(@get_magic_quotes_gpc()=='1') $path=stripslashes($path);
  return $path;
 }
?>

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

Приведём иллюстрацию их использования, написав некий гипотетический файл host.php:

<?php
 echo "<br>\$_SERVER['HTTP_HOST']=".$_SERVER['HTTP_HOST'];
 if (empty($_SERVER['HTTP_REFERRER'])) { 
  echo "<br>\$_SERVER['HTTP_REFERRER'] is empty";
  if ($_SERVER['HTTP_HOST']=='127.0.0.1') echo '<br>It is localhost';
  else echo '<br>It is host http://' . $_SERVER['HTTP_HOST']; 
 } 
 else echo "<br>\$_SERVER['HTTP_REFERRER']=".$_SERVER['HTTP_REFERRER'];
 echo "<br>\$_SERVER['REMOTE_ADDR']=".$_SERVER['REMOTE_ADDR'];
 
 echo '<br><a href="hostform.php">Relative link to form</a>';
 
 //Защищаем код с помощью $HTTP_REFERER
 $my_url='http://www.myserver.com'; //URL, с которого можно обращаться к скрипту
 
 if (isset($HTTP_REFERER)) $referer=$HTTP_REFERER; //Если register_globals=off, то $HTTP_REFERER в принципе не определена
 else if (isset($_SERVER['HTTP_REFERER'])) $referer=$_SERVER['HTTP_REFERER'];
 else $referer=getenv("HTTP_REFERER");
 //В принципе, эти 3 строки сегодня можно заменить на одну: $referer=getenv("HTTP_REFERER");
 
 $pattern='#^'.$my_url.'#'; //Шаблон - в начале адрес $referer должен быть $my_url
 if (!preg_match($pattern,$referer)) {
  echo "<br>Hacker detected :) Referer is \"$referer\"";
  exit;
 }
 else {
  echo "<br>OK. Referer is \"$referer\"";
  //Здесь можно обрабатывать данные
 }
?>

Этот файл ссылается на файл hostform.php, содержащий код формы:

<form action="host.php" method="get">
 <table align="center" border="0" cellpadding="4" cellspacing="0" width="90%">
  <tr>
   <td>Your data:</td>
   <td>
    <input type="text" name="name" maxlength="40" size="40" value="">
   </td>
  </tr>
  <tr>
   <td>&nbsp;</td>
   <td>
    <input type="submit" name="action" value="Send"> 
    <input type="reset" value="Reset">
   </td>
  </tr>
 </table>
</form>

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

Почему защита по $HTTP_REFERER ненадёжна?

Тем не менее, я бы такой защитой не пренебрегал, а всё остальное довершит грамотная настройка сервера, на котором хостится скрипт.

 Скачать файлы из этой статьи в архиве ZIP (4 Кб)

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

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

Для проверки этого факта может использоваться какой-либо параметр, передаваемый всем страницам стандартными методами GET или POST (ненадёжно), IP-адрес пользователя (ненадёжно и вообще не рекомендуется для авторизации, хотя бы по причине существования прокси-серверов) или же суперглобальный массив PHP $_SESSION (нормально при грамотном использовании).

В несложных случаях можно обойтись и просто скрытым параметром формы HTML, если применить примерно такой подход: после авторизации пользователя скрипт, проведший авторизацию, генерирует достаточно длинное случайное число, называемое random uid (Random User IDentificator):

mt_srand((double)microtime()*1000000);
$uid=mt_rand(1,1000000);

Это число:

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

<input type="hidden" name="uid" value="743927">

Значение uid невидимо для пользователя, но передается защищённой части приложения. Скрипт сличает переданный ему uid с uid'ом, хранящимся в локальной базе и либо выполняет свою функцию, либо запрещает делать это.

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

Имеет смысл подумать и о защите кода от слишком частого обращения с той же сессии или того же IP-адреса. Это позволит, к примеру, сделать простейшую защиту от DDOS-атак на Ваши страницы с формами. Общий подход таков - для каждого клиента, идентифицированного сессией или IP, сохраняется информация о времени последнего посещения, полученного стандартной функцией time или microtime, а перед выдачей клиенту содержимого скрипт читает эту информацию и сравнивает текущее время системное с временем, сохранённым для этого клиента (если таковое есть). Если прошло недостаточно времени - выходим из скрипта. Если время для данного клиента не найдено - добавляем его в лог. При чтении и разборе сохранённой информации о посещениях целесообразно сразу же её "чистить", удаляя устаревшие записи, иначе со временем лог разрастётся и начнёт сильно "тормозить".

Простой и неплохой приём - делать паузу в секунду перед проверкой входа в систему (или перед антиспамовой проверкой и добавлением сообщения, если форма не для входа, а для отправки):

sleep (1);

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

Ну и не стоит забывать про другие способы защиты, например, $_COOKIES (сгенерируйте куки-файл и проверяйте его наличие, чужим куки-файл не давайте, и пароли не нужны), файл .htaccess, алгоритмы авторизации через код сервера 401 и просто отключение директивы register_globals в файле конфигурации php.ini

Рейтинг@Mail.ru

вверх гостевая; E-mail
Hosted by uCoz