Добро пожаловать к нам опять, в цикл статей по основам PHP. В прошлый раз, я распространялся о потенциальных брешах в безопасности, которые могут возникнуть при использовании системных вызовов в PHP-скриптах (и о некоторых методах защиты). Эта мини-серия статей посвящёна подводным камням при написании защищённых PHP-приложений; однако же, в последней моей статье я не буду рассказывать вам о каких-либо новых дырах в безопасности. Я считаю, что лучшим завершением нашего разговора о безопасности будут рассказ о средствах обработки и протоколирования ошибок, и краткая ретроспектива всего изложенного в этой мини-серии. Безопасность и ведение логов Злоумышленнику, чтобы использовать ваш код в своих целях, нужно сначала определить его слабые места. Это достигается зондированием вашего приложения и сбором как можно большего количества информации о нём. Самый распространённый и эффективный способ - это попытки вызвать ошибки приложения.. Предположим, например, что злоумышленник ввёл некорректные данные в поле login-формы и спровоцировал следующее стандартное сообщение об ошибке в PHP: PHP: Notice: Undefined index: content in /usr/local/apache/htdocs/index.php on line 22 Что нам может поведать это сообщение? Очевидно, то, что в коде есть массив с индексом content, неопределённый в строке 22 файла index.php. Благодаря этому сообщению, у злоумышленника есть подсказка о потенциальной дыре, через которую можно замусорить данные приложения. Может быть, это неопределённый индекс где-нибудь в $_GET или в $_POST. В принципе, смоделировать разные контексты и определив, как меняется при этом номер строки, где возникает ошибка, вы можете даже воссоздать общую идею работы скрипта. Кроме того, вид сообщения может также предоставить более подробную информацию о расположении файлов, с которыми работает скрипт (если мы работаем с функциями файловой системы), формат запросов (если работаем с базами данных), и прочие данные. Начнём с того, что обычно пользователям просто не нужно получать сообщение об ошибке со всеми подробностями, если что-то пошло не так. Ну а при определённых обстоятельствах такие подробности могут стать дырой в системе безопасности. Первый шаг в решении этой проблемы - всевозможные меры по сокращению появления ошибок в скриптах. В каждом уважающем себя PHP-приложении все переменные должны быть определены, или, по крайней мере, проверены isset()-ом, если речь идёт о суперглобальных переменных. Однако сколько бы не было продумано и предусмотрено в вашем приложении, предполагать, что вы учли любые обстоятельства, нереалистично. Поэтому, в "охраняемых" приложениях должны быть реализованы системы обработки и регистрации ошибок. Механизм регистрации ошибок в PHP Смысл систем обработки и регистрации ошибок, по крайней мере, применительно к вопросам безопасности, - это отказывать злоумышленникам в доступе к информации о вашем приложении, но при этом предоставлять её разработчику. Правильная система регистрации ошибок запишет попытки скомпрометировать систему защиты вашего приложения и предоставит вам сведения о том, как и где надо усилить меры защиты. Система регистрации ошибок в PHP может быть настолько простой или сложной, насколько вы пожелаете. В самом PHP есть выбор из нескольких встроенных механизмов обработки и регистрации ошибок. Например, PHP по умолчанию все ошибки выполнения скрипта выдаёт в браузер, но может быть сконфигурирован так, чтобы ошибки регистрировались, но не отображались. Это поведение контролируется из файла настройки php.ini директивами log_errors и display_errors. Включайте и выключайте сообщения об ошибках в зависимости от ваших программерских нужд. Обычная практика - это отображение ошибок без их регистрации на стадии разработки и отладки приложения. Для готового продукта всё наоборот: регистрация без отображения. Как сообщения об ошибках PHP вываливаются в браузер, вы видели, а где PHP ведёт лог, если регистрация ошибок включена? Поведение механизма регистрации ошибок в PHP контролируется другой директивой, error_log. Значение может быть выставлено на имя файла, или на строку "syslog", или вовсе не выставлено (по умолчанию). В последнем случае, то есть когда значение error_log в php.ini не выставлено, то логи PHP ведутся за счёт средств web-сервера (например, Apache-вский error-log). Если значение выставлено на имя файла, то PHP будет писать лог в указанный файл, пока будут позволять системные разрешения. Если значение выставлено на ключевое слово syslog, то лог PHP будут вестись средствами журналирования операционной системы. Для UNIX-систем это стандартный системный журнал [syslog], для систем Windows NT и XP это журнал событий [event log]. PHP сам позаботится о регистрации ошибок, если вы используете встроенный обработчик ошибок, однако для своего обработчика (о нём речь пойдёт далее в статье) вам придётся всё сделать своими руками. Для подобных деяний PHP предлагает функцию error_log() со следующим синтаксисом: PHP: error_log($message [, $message_type [, $dest [, $extra_info]]]); В зависимости от выставленного значения необязательного параметра $message_type (ноль по умолчанию), произойдёт одно из нижеперечисленного: если $message_type равен нулю, то сообщение об ошибке $message будет записано в лог, указанный в директиве конфигурации error_log. если $message-type равен 1, то сообщение об ошибке $message будет отправлено по электронной почте на адрес, указанный в параметре $dest. Любые дополнительные почтовые заголовки можно указать в параметре $extra_info. если $message_type равен 3, то сообщение об ошибке $message будет записано в файл, указанный в параметре $dest. Примечание: Вы наверняка заметили, что для значения $message_type равного 2 поведение не предусмотрено. Это рудимент от PHP версии 3, где удалённая отладка поставлялась в стандартном релизе как его часть. Для PHP4 это уже не так. Функцию error_log() можно использовать для ведения логов в любой части приложения, но, как правило, она применяется как часть своего обработчика ошибок. Примеры - в мануале. Классификация ошибок в PHP Понимание классификации ошибок PHP, почти также важнО как правильная настройка системы регистрации ошибок. Данная модель определяет, какие типы ошибок регистрируются, как и когда происходит регистрация. Чтобы осмыслить саму модель классификации, вам нужно знать, какие типы ошибок вы можете получить в PHP, и каково значение каждой из них. Эти знания вы можете почерпнуть из мануала или из представленного ниже списка: E_ERROR означает, что в самом PHP или в одном из его расширений возникла серьёзная внутренняя проблема (например, невозможность выделить память). E_WARNING обычно выдаётся с целью привлечь внимание к потенциальной ошибке в коде, из-за которой он возможно будет работать не так, как задумано. Пример: какой-либо встроенной функции передаётся скалярное значение вместо ожидаемого составного, например массива. E_NOTICE - это наименее значительная встроенная ошибка PHP. Этот тип почти никогда не означает, что приложение работает неправильно. Сообщения об ошибках этого типа призвано предупредить разработчика о таких вещах, как использование переменных до инициализации. E_CORE_ERROR по фатальности равно E_ERROR, но выдаётся только при запуске ядра PHP. E_CORE_WARNING - не фатальный коллега E_CORE_ERROR и схож с E_WARNING. выдаётся только при запуске ядра PHP. E_COMPILE_ERROR означает серьёзную ошибку во время компиляции движком Zend Scripting Engine. E_COMPILE_WARNING предупреждение, нефатально, как и E_WARNING, генерируется во время компиляции движком Zend Scripting Engine. E_USER_ERROR зарезервирован исключительно для использования с функцией trigger_error() (см. далее в статье). По умолчанию обработка та же, что и у E_ERROR: сообщение об ошибке и прекращение исполнения. E_USER_WARNING зарезервирован исключительно для использования с функцией trigger_error().По умолчанию обработка та же , что и у E_WARNING. E_USER_NOTICE зарезервирован исключительно для использования с функцией trigger_error(). В отличие от E_NOTICE, который игнорируется по умолчанию, PHP выдаст сообщение о E_USER_NOTICE пользователю. Устанавливаем режим вывода сообщений об ошибках Директива конфигурации error_reporting определяет, какие сообщения об ошибках пишутся в лог или выбрасываются в браузер, когда оные ошибки случаются. Эта директива представляет собой "битовое поле", а это означает, что типы сообщений можно как угодно комбинировать с помощью логических операторов AND, OR, и NOT.В файле php.ini символ амперсанда (&) означает AND, символ конвейеризации (|) означает OR, а тильда (~) означает NOT. Таким образом, чтобы отображались или писались в лог только серьёзные ошибки, ваша директива примет следующий вид: error_reporting = E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR Помимо стандартных типов ошибок директива error_reporting принимает специальный тип, E_ALL, представляющий все возможные типы ошибок сразу, то есть как будто вы OR-ами соединила все стандартные типы. Приведённая ниже директива заставляет отображать или писать в лог все типы ошибок, кроме ошибок из коллекции E_USER: error_reporting = E_ALL & ~E_USER_ERROR & ~E_USER_WARNING & ~E_USER_NOTICE По умолчанию директива error_reporting сообщает в браузер или лог обо всех ошибках, кроме E_NOTICE. Вне зависимости от конфигурации PHP в части отлова ошибок, есть несколько способов контроля ситуации во время исполнения скрипта. Самый простой способ - использовать оператор подавления сообщений, @. Поставьте @ перед каким-либо выражением и PHP втихомолку проигнорирует ошибку, если таковая случится при вычислении выражения. Например: PHP: <?php echo @$myvar; ?> не выкинет сообщение класса E_NOTICE, даже несмотря на то, что $myvar неопределена. Ошибку эту PHP проигнорирует и не напишет ничего. Другой способ изменить настройку управления ошибками в PHP - это использовать функцию error_reporting(). Данная функция непосредственно изменяет "внутреннее" значение конфигурационной директивы error_reporting. Синтаксис функции таков: PHP: error_reporting([$error_value]) где факультативный параметр $error_value - это новое значение битового поля. В PHP определены константы для всех приведённых выше классов ошибок, и из них можно составить комбинацию с помощью логических операторов PHP. Результатом работы следующей инструкции станет то, что PHP будет сообщать только о серьёзных ошибках, как и в одном из предыдущих примеров: PHP: <?php error_reporting(E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR); ?> Независимо от того, какое значение будет передано в качестве параметра в error_reporting(), на выходе мы всегда будем иметь целое число, равное ранее установленному значению директивы error_reporting. Такое поведение позволяет вам применить особый режим вывода сообщений об ошибках для небольшой части вашего приложения и с лёгкостью восстановить предыдущий режим: PHP: <?php $old_error = error_reporting(0); /* Сообщения об ошибках больше не генерируем */ /* тут какой-то код */ error_reporting($old_error); /* восстанавливаем предыдущий режим вывода сообщений об ошибках. */ ?> Обработчики ошибок, определяемые программистом Помимо встроенных средств обработки ошибок PHP также позволяет вам написать ваш собственный обработчик. Определённый вами лично обработчик ошибок позволяет вам осуществлять полный контроль над тем, как ваше web-приложение будет реагировать на ошибки, сгенерированы ли они PHP или сброшенные функцией trigger_error(). Чтобы задействовать определённую вами обработку ошибок, необходимо определить следующим способом: PHP: <?php function my_handler($error_code, $error_msg [, $error_file [, $error_line [, $vars]]]) { /* Собственно реализация обработчика */ } ?> Как описано в нашем примере, обработчик ошибок должен принимать параметры $error_code и $error_msg; первый - это класс ошибки (например, E_ERROR), а второй - это сообщение, привязанное к данному классу. Ваш обработчик ошибок способен принимать до трёх дополнительных параметров: $error_file, $error_line, и $vars. Первые два - это PHP-файл и строка в нём, где ошибка имела место. Последний параметр, $vars, представляет собой ассоциативный массив, в котором содержатся имя и значение каждой переменной, которые были в области видимости на момент возникновения ошибки. Создав такую функцию, её надо зарегистрировать в PHP как действующий обработчик ошибок. Для этого используйте функцию set_error_handler(), синтаксис её таков: PHP: set_error_handler($func_name) где $func_name - это строка с именем определённой вами функции (то есть, для моего примера, my_handler). Эта функция вернёт название предыдущего обработчика, то есть при желании можно это название сохранить и потом вернуть всё на место. При использовании определённого вами обработчика ошибок, держите в голове следующие моменты: Действие вашего обработчика не распространяется на "фатальные" ошибки. Ваш обработчик будет вызываться только для ошибок класса E_WARNING, E_NOTICE и коллекции E_USER. Для всех прочих ошибок будет вызываться встроенный обработчик, как если бы вы вовсе не определяли свой альтернативный обработчик. Если задействован обработчик, определённый программистом, PHP будет вызывать его при возникновении любой из вышеперечисленных ошибок, конфигурационная директива error_reporting игнорируется. Какая-либо реакция на error_reporting() определяется также в обработчике. Если только исполнение скрипта не прерывается в обработчике с помощью die() или exit(), скрипт продолжит работу, невзирая на класс ошибки. Прекращение работы скрипта при необходимости определяется в обработчике. Последняя функция на сегодня, trigger_error(). Эта функция используется для генерации ошибки, определённой разработчиком. Синтаксис функции таков: PHP: trigger_error($msg [, $error_code]) где $msg - это строка с сообщением и описанием об ошибке, а факультативный параметр $error_code - класс ошибки из коллекции E_USER. Если второй параметр опускается, PHP автоматически применяет E_USER_NOTICE. Функция trigger_error() предназначена для использования в связке с обработчиком, определённым программистом; однако если такой обработчик не был определён, PHP среагирует на ошибку через стандартный обработчик. В качестве резюме В завершение этой мини-серии, посвящённой некоторым аспектам безопасности в PHP, я хотел бы ещё раз обозначить тезисы трёх статей. При написании web-приложения на PHP (да и любых других приложений на любом же языке), самое большее, что вы можете сделать для повышения уровня безопасности в вашем приложении - это держать в голове все возможные пути компрометации оной безопасности. Используются ли системные вызовы? Что делается для их защиты от непредусмотренного использования? Как приложение будет реагировать на некорректные данные от пользователя? Какие меры предусмотрены для фильтрации данных, полученных от пользователя? При разработке приложения вы должны всегда задавать себе все эти вопросы. В конечном счёте, любой текст (включая тот, что у вас перед глазами) может только лишь чему-то научить. Вы получили некоторые начальные знания о ведении логов и проверке данных на корректность, вам же и решать, будете ли вы применять эти знания в разработке ваших приложений. Тщательность и внимательное отношение к каждой детали - лучшие инструменты разработчика при создании системы защиты для приложения. Конечно, злоумышленники используют стандартные средства, чтобы заставить ваше приложение повести себя непредвиденным образом. Однако сама природа злого умысла говорит о том, что всегда найдётся то, чего вы не предусмотрели. А раз найдено, значит использовано. В моей следующей статье я немного сбавлю обороты и от безопасности перейду к обзору различных инструментов для работы с данными и управления ими. Итак, до встречи, счастливо покодить! Джон Коггсхол (John Coggeshall): специалист и один из создателей PHP. Недосыпания по причине PHP начались около пяти лет назад. Автор: Данил Миронов