Давайте поговорим о безопасности при обработке данных, связанной с выводом отладочной информации в браузер посетителя.
Очень часто, при обработке клиентских данных весь вывод отладочной информации, как и ошибок, программисты отправляют в браузер посетителя, совершенно не задумываясь при этом о дальнейшем профилировании проекта и его усовершенствовании, а также о том, что своими же руками они предоставляют конфиденциальные технические данные работы проекта любому желающему, да и попросту раздражают этим рядового пользователя. Это при том, что 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");}
Примечание автора: правилом хорошего тона при перехвате и обработке ошибок (особенно критических, которые ведут к невозможности предоставить посетителю необходимую информацию, за которой он, собственно, и пришел к Вам на сайт) является создание для каждого вида подобных ошибок статической страницы с объяснением на человекопонятном языке без технических данных сути проблемы (также на такой странице можно оставить контакты админитрации) и перенаправление посетителя на нужную страницу в ходе возникновения ошибки.