Сер 282013
 

Давайте поговорим о безопасности при обработке данных, связанной с выводом отладочной информации в браузер посетителя.

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

Кроме этого часть ошибок так и остается «за кадром», которые можно выявить и устранить уже в процессе работы, тем самым повышая стабильность сайта и избегая потери новых посетителей, которые, столкнувшись с определенными трудностями и ошибками, попросту уйдут на другой ресурс

Итак, перейдем к сути вопроса.

Допустим, у нас есть страница, отображающая новости раздела, в контексте которой согласно id страницы $_GET[‘ limitfrom’] категории $_GET[‘id’] мы выбираем количество новостей $_GET[‘limit’] из связанной таблицы материалов `news`.

Обычно (утрировано и упращенно) это выглядит, например, так:

<?php
$id         = $_GET['id'];
$limitfrom  = $_GET['limitfrom'];
$limit      = $_GET['limit'];
$sql        = 'SELECT * FROM news WHERE category_id ='
                  . (int)$id.'  LIMIT '. (int)$limitfrom.','.(int)$limit;
$resource   = mysql_query($sql);
If($resource && is_resource(resource)){
    //обработка данных
} else {
   echo mysql_error();
   die();
}
?>

Пример «взят» не из воздуха, вот так, приблизительно, но в более сложной форме выглядит разбитие на постраничную навигацию в разделах известного новостного движка DLE. А также вот так (echo mysql_error(); die()) выводится, зачастую, в браузер посетителя возникновение mysql ошибки в Joomla 1.5 и более новых версий (чуть сложнее, но смысл остается тот же).

Здесь была осознанно допущена одна оплошность. Дело в том, что данный вариант будет работать отлично! И в повседневной работе программист не увидит выполнение echo mysql_error().

Но почему не следует выводить в браузер отладочную информацию и ошибки исполнения PHP?

Итак, первое, из за того, что мы обходимся без проверки, что же приходит со стороны клиента, например, отрицательные значения $id и $limit, которые допускаются для типа данных integer, или несуществующие значения приведут к выводу отладочной информации mysql_error(). Итак, ясным по белому, мы дадим информацию злоумышленнику о существующих таблицах и пищу для размышления, как же проще осуществить mysql injection в другом месте, где используются эти же таблицы при составлении запросов. Или же в некоторых случаях покажем посетителям техническую информацию, которую видеть они не должны. С одной стороны здесь нам помогут более строгие проверки входных данных. Перепишем этот пример с использованием выше сказанного:

 

<?php
$id         = abs((int)$_GET['id']);
$limit      = abs((int)$_GET['limit']);
$limit      = $limit ? $limit : 10;
$limitfrom  = abs((int)$_GET['limitfrom']);
$sql        = 'SELECT * FROM news '
.($id && !empty($id) ? 'WHERE category_id='. $id
: '')
.( $limitfrom && !empty($limitfrom) ?
'  LIMIT '. $limitfrom.', '. $limit.''
: '  LIMIT '. $limit.'');
$resource   = mysql_query($sql);
If($resource && is_resource(resource)){
    //обработка данных
} else {
   echo mysql_error();
   die();
}
?>

Мы предотвратили, как минимум, возникновение нескольких исключительных ситуаций. Мы знаем, что нам нужны только лишь целые положительные значения $id,$limit – поэтому используем явное приведени типов для этих переменных, оператор приведения к целому (int) и функцию получения целого числа abs(). Также мы знаем, что количество выбранных новостей не должно быть равно нулю, поэтому определяем минимальный «порог» в 10 новостей $limit = $limit ? $limit : 10;, формируя запрос, мы проверяем данные, и если они есть и отличны от 0, то добавляем их в mysql запрос по частям.

Все бы хорошо, но и здесь могут быть подводные камни. И все равно существует некоторая вероятность того, что при манипуляции с входными данными злоумышленник увидит вывод той самой информации echo mysql_error();. Зачастую это может получиться даже чисто случайно.

Другой распространенный пример. Многие программисты добавляют вывод технической информации еще на этапе подключения к базе данных. Что, в случае отсутствия соединения с базой данных (сверх нагрузки/временной недоступности сервера баз данных) также выводит отладочную информацию посетителям. При этом там, зачастую, могут в простом тексте встречаться даже логин/название базы/хост подключения к базе данных. А сам программист может никогда и не увидеть подобной информации, и даже не узнать о временном полягании проекта (например из за превышения числа подсоединений к базе данных в случае с увеличением аудитории, как итог: при частом повторении проблемы потеря новых посетителей, раскрытие важных технических данных).

А ведь сбор такой информации бывает полезным для профилирования приложения и дальнейшего его усовершенствования (обеспечения стабильности работы). Для выявления «подводных» камней в стабильной работе сайта.

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

Какие функции и данные для логирования в PHP могут быть полезны?
trigger_error – для генерации исключения.
ini_set , error_reporting – для подавления вывода ошибок
set_error_handler – для установки собственной функции «перехвата» ошибок
register_shutdown_function – для отлова критических ошибок (которая работает, как нужно, начиная с php 5.2)
error_get_last – функция получения последней ошибки, произошедшей в скрипте, очень полезна в случае прерывания скрипта из за возникновения критической ошибки
$_SERVER – суперглобальный массив – источник информации – где и когда и какая ошибка произошла
getenv – функция для получения значения переменных среды окружения сервера, в том случае, если какие-то данные в суперглобальном массиве $_SERVER отсутствуют
Давайте напишем свой перехватичк исключительных ситуаций для дальнейшего логирования данных об ошибках. Суть заключается в следующем, мы переопределим вывод отладочной информации с помощью собственной функции в текстовый файл в определенной папке /errors, чтобы его не смогли прочесть извне, присвоим ему расширение .php (можно и при помощи запрета прямого доступа извне к этой папке, но данный метод более универсален для любых серверов и не требует дополнительной настройки окружения сервера) и добавим первой строчкой , название же зададим ему, совпадающее с текущей датой, чтобы было проще ориентироваться. В итоге один файл ошибок – одна дата.Также мы добавим «перехват» критических ошибок. И при помощи функции trigger_error для логирования непредвиденных ситуаций и генерации собственных ошибок типа E_USER:

Примечание автора: желательно подключить подобный код как можно раньше, еще на этапе инициализации созданного php приложения:

<?php
error_reporting(0);
ini_set('error_reporting','0');
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
ini_set('ignore_repeated_errors', '1');
define ('ROOT_PATH', dirname(dirname(__FILE__))."/");
//запретить/разрешить вывод ошибок
define('_ERR_HANDLING',true);
//где будем хранить файлы ошибок
define('_ERR_DIR',ROOT_PATH.'errs/');
function error_reporting_log($error_num, $error_var=null,
$error_file=null, $error_line=null) {
$error_desc= '';
$error_desc  = 'Error';
switch ($error_num){
case E_WARNING:
$error_desc = 'E_WARNING';
break;
case E_USER_WARNING:
$error_desc = 'E_USER_WARNING';
break;
case E_NOTICE:
$error_desc = 'E_NOTICE';
break;
case E_USER_NOTICE:
$error_desc = 'E_USER_NOTICE';
break;
case E_USER_ERROR:
$error_desc = 'E_USER_ERROR';
break;
case E_ERROR:
$error_desc = 'E_USER_ERROR';
break;
default:
$error_desc  = 'E_ALL';
break;
}
$date_file = date('y-m-d H:I:S');
$logfile= LOG_FILE;
$url = $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
$date_time = date('d.m.y - H:i:s');
$ip= isset($_SERVER['REMOTE_ADDR']) &&
!empty($_SERVER['REMOTE_ADDR']) ?
$_SERVER['REMOTE_ADDR']
: getenv('REMOTE_ADDR ') ;
$from= isset($_SERVER['HTTP_REFERRER'])&&
!empty($_SERVER['HTTP_REFERRER'])?
$_SERVER['HTTP_REFERRER']
:getenv('HTTP_REFERRER');
$errortext = $error_desc.': '.$error_var."\t".'
Line: '.$error_line."\t".'
File: '.$error_file."\t".'
Link: '.$url."\t".'
Date: '.$date_time."\t".'IP: '.$ip."\t".'
FROM:'.$from."\n";
unset($from,$error_desc,
$error_var,$error_line,
$error_file,$url,$date_time,$error_write);
$secuire= '<?php die("Forbidden."); ?>';
if(is_file($logfile)&&is_writeable($logfile)){
$fp = fopen($logfile,'r');
if($fp && is_resource($fp)){
$strings= fgets($fp);
if(isset($strings)&&!empty($strings)
&&strpos($strings,$secuire)===false){
unlink($logfile);
}
fclose($fp);
};
unset($fp);
}
if(!is_file($logfile)){
$dir= dirname($logfile);
if(is_dir($dir)&&is_writable($dir)){
$fp     = fopen($logfile,'w+');
if(is_resource($fp)){
flock($fp,LOCK_EX);
fwrite($fp,$secuire."\n");
flock($fp,LOCK_UN);
fclose($fp);
$fp= null;
}
unset($dir,$fp);
}
}
unset($secuire);
if(is_file($logfile)&&!is_writable($logfile)){
chmod($logfile,0775);
}
if(is_file($logfile)&&is_writeable($logfile)){
$fp     = fopen($logfile,'a+');
if(is_resource($fp)){
flock($fp,LOCK_EX);
fwrite($fp,$errortext);
flock($fp,LOCK_UN);
fclose($fp);
$fp= null;
unset($fp);
}
}
unset($logfile);
                return true;
}
function err_handler(){
if(_ERR_HANDLING){
$error_reporting= '';
$error_reporting= ini_get('error_reporting');
$error_reporting= $error_reporting?$error_reporting:E_ALL;
error_reporting(E_ERROR);
$date_file = date('dmY').'.php';
$dir= _ERR_DIR;
$path= $dir.$date_file;
$logfile= '';
if(!is_dir($dir) || !is_writable($dir)){
if(is_dir($dir)&&!is_writable($dir)){
chmod($dir,0775);
} else if(!is_dir($dir)){
$isdir= false;
$isdir= mkdir($dir,0775);
}
if(!$isdir&&!is_writable($dir)){
$dir= ROOT_PATH;
$path= $date_file;
}
}
if(is_dir($dir) && is_writable($dir)){
if(!is_file($path)){
$fp= fopen($path,'w+');
if($fp && is_resource($fp)){
$secuire= '<?php die("Forbidden."); ?>';
flock($fp,LOCK_EX);
                fwrite($fp,$secuire."\n");
                flock($fp,LOCK_UN);
                fclose($fp);
                $fp= null;
unset($secuire);
}
}
if(is_file($path) && !is_writable($path)){
chmod($path,0775);
}
if(is_file($path) && is_writable($path)){
ini_set('display_errors',0);
set_error_handler('error_reporting_log', (E_ALL & ~E_NOTICE));
$logfile= $path;
define('LOG_FILE',$logfile);
}
unset($date_file,$dir,$path,$logfile);
}
error_reporting($error_reporting);
unset($error_reporting);
}
}
function critical_error(){
$error = error_get_last();
if(is_array($error) && sizeof($error) && isset($error['type'])
&& $error['type'] == E_ERROR && !empty($error['message'])
&& !empty($error['file']) && !empty($error['line'])){
error_reporting_log($error['type'],
$error['message'],$error['file'],$error['line']);
}
}
if(function_exists('register_shutdown_function')
&& function_exists('error_get_last')
&& _ERR_HANDLING){
register_shutdown_function('critical_error');
}
err_handler();

Что здесь происходит? При помощи функции ini_set и error_reporting мы подавляем вывод ошибок в браузер посетителя, константами определяем местоположение нашей директории для хранения логов ошибок define(‘_ERR_DIR’,ROOT_PATH.’errs/’);. Определенная в самом начале константа (bool) _ERR_HANDLING – при значении true разрешит перенаправление всех наших ошибок в файлы, при значении false дальнейшие инструкции выполняться не будут. Для перехвата самих ошибок используется функция error_reporting_log, которая принимает тип ошибки, сообщение об ошибке, имя файла, где произошла ошибка, и строку, в которой она произошла. В ней мы собираем кроме этой информации, и другие полезные данные. А именно:

$_SERVER[‘HTTP_REFERRER’] – откуда пришел посетитель, с какой страницы?
$_SERVER[‘REMOTE_ADDR’] – ip посетителя,
$_SERVER[‘HTTP_HOST’]. $_SERVER[ ‘REQUEST_URI’] – полный адрес страницы вместе со строкой запроса, на которой возникла ошибка
date(‘y-m-d H:I:S’) – текущее время возникновения ошибки.
Зачастую, подобной информации хватит с головой и для логирования и исправления ошибок, которые были не замечены на этапе разработки, так и для отлова злоумышленников. Но есть один тип ошибок, который не будет перехвачен. Это критические ошибки PHP E_ERROR, которые приводят к прерыванию php скрипта . Начиная с версии PHP 5.2 для перехвата критических ошибок можно определить собственную функцию при мопощи register_shutdown_function. Мы создаем собственную функцию для этих целей critical_error, в которой при помощи функции error_get_last получаем информацию о текущей ошибке. Так как данная функция будет всегда выполняться после завершения php скрипта, мы логируем информацию только об ошибках с типом E_ERROR.

Можно было бы использовать функцию error_log с указанием пути к источнику логирования, но было отдано предпочтение простым файловым функциям, при помощи которых можно установить функцией flock приоритетное эксклюзивное запирание во избежание попыток одновременной записи в логфайл несколькими скриптами (и возникновения «битых» файлов).

Давайте на примере все того же скрипта с mysql рассмотрим принцип логирования mysql ошибок (впрочем, по такому же принципу можно построить логирование любых внештатных ситуаций):

<?php
$id         = abs((int)$_GET['id']);
$limit      = abs((int)$_GET['limit']);
$limit      = $limit ? $limit : 10;
$limitfrom  = abs((int)$_GET['limitfrom ']);
$sql        = 'SELECT * FROM news '
.($id && !empty($id) ?
'WHERE category_id='. $id
: '')
.( $limitfrom && !empty($limitfrom)
? '  LIMIT '. $limitfrom.', '. $limit.''
: '  LIMIT '. $limit.'');
$resource   = mysql_query($sql);
If($resource && is_resource(resource)){
    //обработка данных
} else {
  $string                 = print_f(
'Ошибка mysql при получении новостей из категории с ID: %d,'
  . 'на странице:  %d , '
  . 'информация о запросе:  %s,'
  . ' информация об ошибке: %s',
  $id,$limitfrom, $sql, mysql_error());
   trigger_error($string,E_USER_ERROR);
   die();
}
?>

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

Примечание автора: также для генерации и перехвата ошибок Вы можете использовать конструкции try{}catch(Exeption $e){ thrown(throw new Exception("$name contains the word name");}

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