После прочтения статьи Securing PHP, написанной Джеймсом Каннингемом, я подумал, что неплохо бы собрать воедино несколько тезисов об использовании PHP. Имейте в виду, что я не эксперт по вопросам безопасности. Однако эта статья содержит несколько отправных точек по предотвращению заражения экcплоитами, повышению защищенности PHP-приложений и прочим вещам, которые я считаю самыми полезными из своей практики. Ваша оценка может (и, вероятно, будет) колебаться: нормально воспринимать все с недоверием. И это не зависит от того, где вы прочтете такую информацию — здесь или в другом месте. Это не столько контрольный список конкретных действий, сколько набор правил, на которые надо обратить внимание при программировании.
Одержимость PHP
Первое, с чем вы столкнетесь при начале работы с PHP — отвратительная репутация интерпретатора. Вы услышите много высказываний, в том числе: «он не масштабируется», «это не настоящий язык», «в php нет X, поэтому php – отстой», «он не безопасен», или «это парадокс Блаба и ты слишком глуп, чтобы понять парадокс Блаба». С точки зрения времени, проведенного за изучением языка, все это полная чушь. Тем не менее, в данных высказываниях есть доля правды — хотя это вообще не ошибка в PHP. Вина за все причисленные «косяки» лежит исключительно на разработчиках, использующих его. Может вас это и не утешит, но другие языки и фреймворки страдают ровно от тех же проблем. Просто это не афишируется. Ошибки в PHP-приложениях кажутся настолько огромными и многочисленными из-за того, что язык широко распространен и достаточно мощен.
Основы: как работает PHP
Поскольку PHP является таким доступным и повсеместным, многие «сообразительные» люди копируют и вставляют готовые скрипты в одном приложении. Таким «разработчикам» в более совершенном мире напрочь запретили бы прикасаться к исходному коду. Поступая таким образом ты обязан знать, как работает система изнутри.
На самом фундаментальном уровне веб-сервер обрабатывает PHP-код после приема запроса из браузера пользователя. Это на самом деле очень простая концепция, применяемая в большинстве случаев. В настоящее время большинство серьезных веб-серверов сконфигурированы для интерпертирования PHP. Обработка PHP-кода делается только со стороны веб-сервера. После обработки результат передается в браузер пользователя. Но так было не всегда. В прошлом веб-серверы часто запускали полный вариант PHP-кода при каждом запросе. Такой подход может показаться вам неэффективным, и вы будете абсолютно правы. Когда люди, наконец, поняли, насколько расточителен этот метод, они приняли нынешнюю модель повторного использования PHP-экземпляров после завершения его работы. Тем не менее, на некоторых хостингах все еще используется старая модель.
Чистый лист
Каждый разработчик должен усвоить один принципиальный момент. Среда создается только один раз за PHP-запрос. Что бы вы ни делали в течение последних запросов, следующий запрос к веб-серверу начнется с абсолютно чистого листа. Эта ситуация характерна для многих веб-языков. Другие оперируют с постоянной окружающей средой. При работе с PHP это ограничение позволяет выполнять одни вещи удивительно просто, а с другими могут возникнуть проблемы. Все зависит от ваших требований. Если взглянуть с положительной стороны, это позволяет разработчикам смотреть на каждый запрос, как на изолированную проблему. Кроме того, гораздо труднее сделать ошибку, которая обрушивает весь сервер. Меньше расходуется память и другие ресурсы, которые находятся в состоянии выполнения. С другой стороны, такое ограничение означает, что восстановление среды для каждого запроса требует вычислительных затрат. Кроме того, разработчики не считают такой уж большой проблемой весьма медленную работу приложения и, соответственно, не стремятся ускорить процессы, забывая о том, что продуктивная работа сторонится на принципе экономии ресурсов, в том числе временных.
Чего следует избегать
В первую очередь всего, что подразумевает процедуру инициализации. Не загружать, не проверять, не подключать и не вычислять ненужное. PHP не сможет реализовать вашу мечту и стать ООП. Избегайте огромных иерархий классов. Имейте в виду, что вся эта структура должна разбираться, а затем создаваться при каждом запросе. Так что во многих случаях лучше иметь максимально плоскую систему классов. Не храните большие объемы данных в переменной $ _SESSION, потому что она тоже должна перегружаться при каждом запросе. Если вы уверены, что ваш веб-сервер не поддерживает кэширование (например, APC), не используйте огромные файлы, наполненные ненужным исходным кодом.
Что нужно делать
Функциональное программирование — не обязательно зло. Оно весьма эффективно срабатывает каждый раз, когда на первый план для вас выходят такие характеристики, как скорость и/или простота. Безусловно, необходимы require()/include() файлы. Рассмотрите возможность использования загрузчика класса (осторожно), чтобы загрузить функциональные модули по требованию.
Что необходимо знать
Профилирование, профилирование, профилирование …
PHP позволяет легко отслеживать количество времени и памяти, которые использует ваше приложение. Отслеживать эти показатели очень важно! Если попытаться оценить их интуитивно, можно прийти к выводу, что скорость выполнения PHP находится где-то между Руби и JavaScript (V8). Тут легко наделать ошибок, которые в конечном итоге сожрут много памяти и/или ценных процессорных ресурсов.
Вам даже не нужны серверные отладчики или специальные инструменты для достижения этой цели. Функция microtime() показывает метку времени в микросекундах, а memory_get_usage() дает общее представление о памяти, затраченной на ваш скрипт. Это облегчает вам проверку двух наиболее фундаментальных ресурсов, задействованных в ключевых моментах при работе с вашим приложением.
Лично я привык использовать очень простую профилирующую функцию, основанную на microtime(). Использование этой функции для профилирования кода позволит вам понять, насколько ужасны определенные операции на самом деле. Например, подключение к базе данных, запуск регулярных выражений. Эти операции тоже затрачивают определенные ресурсы, и вы должны знать, сколько именно. Всегда нужно знать, что происходит внутри системы.
Для более серьезного профилирования и отладки переходите на http://xdebug.org/ но microtime() — по-прежнему быстро дает вам ценную информацию в любой среде PHP.
Ввод и вывод
Одна из наиболее распространенных ошибок — неспособность обезопасить ввод данных в веб-приложение. Важно помнить, что каждый кусок данных, поступающих в ваше приложение, потенциально опасен. В PHP массив $ _REQUEST содержит все параметры, относящиеся к текущему запросу, и важно не доверять им. К сожалению, не существует единого способа обеспечить безопасность данных. Все зависит от того, что вы будете с ними делать. В лучшем случае, обработка пользовательского ввода попадает в одну или несколько нижеследующих категорий. Существующие стандартные методы позволят избежать нежелательных последствий:
>>> Правило безопасности ввода данных №1: Стереть с лица Земли — это единственный способ, быть уверенным! <<<
Отображение данных
В наиболее распространенных сценариях пользователь отправляет какой-нибудь текст в приложение, а приложение, в свою очередь, отображает, что текст появился на сайте. Наивно позволяя пользователю вставить любой текст, вы открываете огромные возможности для атаки на ваш сайт с помощью XSS эксплоитов. К счастью, такого рода данные легко обезвредить. В большинстве случаев, htmlspecialchars() справится со своей задачей, игнорируя угловые скобки и другие проблемные символы. Но в некоторых случаях вы можете позволить пользователю вводить разметку, а не только вставлять простой текст. В теории PHP позволяет задать множество «хороших» тегов, а все остальные фильтровать с помощью функции strip_tags(), но эта функция небезопасна, поскольку позволяет злоумышленникам проникнуть с помощью JavaScript-атрибутов к разрешенным тегам. Это означает, что вам необходимо использовать инструмент, который позволит обрезать эти атрибуты (есть несколько примеров в документации PHP). Задача не из банальных. На самом деле, я считаю, что это одна из главных причин, по которой форумы ввели собственный язык разметки, известный как BBCode.
Текст в базах данных
SQL-запросы базы данных поставляются с небольшим багажом. Часто вам приходится принять информацию от пользователя и выполнять запросы по ней. Например, вы можете хранить и восстанавливать данные в БД. Большинство библиотек баз данных позволяет вам использовать синтаксис вроде этого:
SELECT * FROM articles WHERE id = ?
Хороший вариант — иметь поддержку заполнителей в виде «?». В таком случае вам не нужно дополнительно беспокоиться о том, чтобы сделать содержание переменной безопасным. Вы можете просто передать его как параметр к запросу функции. В зависимости от базы данных и используемой библиотеки это может дать вам и дополнительное преимущество — БД может предварительно скомпилировать запрос и выполнить последующие запросы немного быстрее.
Есть, однако, ситуации, когда вы не можете использовать уровни абстракции или библиотеки. Использование встроенной в MySQL функции тоже возможно, вам просто нужно быть более осторожным. Существует только одна функция, которую гарантированно стоит использовать с целью обезопасить данные — mysql_real_escape_string(). И если сейчас вы используете любую другую функцию, немедленно выключите ее. mysql_real_escape_string() — ваш единственный истинный спаситель.
Динамический код
Это щекотливая тема. PHP позволяет при помощи переменных проделывать гораздо больше операций с вашим кодом, чем многие другие языки. И все-таки, если вы собираетесь использовать эти функции, убедитесь, что тому есть веская причина. Если все сделано правильно, эти функции могут существенно уменьшить сложность кода, но вы должны быть очень осторожны.
Динамический include()
PHP позволяет внедрять файл, указанный в переменной. Вы можете сделать так: include(‘inc/’.$moduleName.’.php’);. В разрез с расхожим мнением я думаю, что эта функция действительно может быть очень полезной — она позволяет ввести очень простой механизм расширения вашего приложения, что позволяет сохранить ваш код чистым. Но нужно помнить, что с такой властью приходит огромная ответственность. В первую очередь вам следует убедиться, что $moduleName не может быть использован для вызова произвольного кода на сервере. Хороший способ обеспечить безопасность этой переменной — использование basename($moduleName), но гораздо эффективнее вырезать любые неалфавитные символы.
Динамические переменные и функции
В PHP вы можете установить содержимое переменной $v, указав ее имя через другую переменную. Например, если вы зададите $nnn = ‘vvv’;, вы можете с помощью $$nnn получить доступ к $vvv. Но подождите, есть еще кое-что. Предположим, у вас есть функция vvv();, вы можете вызвать эту функцию, написав $$nnn();. Очевидно, что это очень мощная штука, так что вы должны убедиться, что «id» переменной (в данном примере $nnn) не может быть использована извне. В отличие от предыдущих методов не существует способа узнать, насколько он устойчив от взлома.
Eval — зло
По каким-то необъяснимым причинам, многие новички в восторге от функции eval(). Это притом, что данная функция являет собой самый яркий за всю историю кодирования пример бесполезности.
Функция eval() позволяет выполнить произвольный код. Разработчики часто используют ее для реализации динамических характеристик (таких, как обработчики событий), которые могут быть заданы пользователями приложения. Опасность здесь очевидна. Если вы позволите вашим пользователям указать специальный код, вам придется очень тщательно изучать, насколько можно доверять каждому из них — ведь они получат доступ ко всем данным на сервере. Я осознаю, что в 99% всех случаев желание использовать eval() даже отдаленно не оправдано. Однако могут быть ситуации, когда еval () необходим, например, в приложениях CMS или мета-программировании проектов. Для начинающих и продвинутых PHP-пользователей есть только одна вещь, касающееся eval (), которую нужно держать в голове: если вы используете его, вы подвергаете себя опасности.
Вызов оболочки
Метод хорош, когда требуется обработать часть данных. Просто вызовите текстовую Unix-команду из вашего приложения PHP. Если вы сделаете это, вы должны знать, что ваш код имеет высокую вероятность быть специфическим для вашей конфигурации сервера. Скорее всего, ваш вызов не будет работать на другой конфигурации. Всякий раз, когда мелкие составляющие вашей оболочки вызова зависят от пользовательских данных, вы должны использовать escapeshellarg() и escapeshellcmd() для очистки этих значений.
Регулярные выражения
Регулярные выражения очень практичны и компактны, кроме того, они могут быть очень производительными, если все сделано правильно. Однако вам не обойтись в таком случае без использования специальных технологий. Вы не можете просто скопировать регулярные выражения из интернета и ожидать, что они заработают в вашем проекте. И вообще, заставить их работать неправильно — да смешного легко. Причем найти ошибку сходу достаточно проблематично. Если вы точно не знаете, какую функцию выполняет регулярное выражение, вам, наверное, не стоит рассчитывать на него в вопросе обеспечения безопасности данных. Даже если вы думаете, что владеете такой информацией, высоки шансы, что у вас возникнут проблемы с вводом данных. Лично я бы посоветовал вам держаться подальше от регулярных выражений связанных с безопасностью, если то же самое можно сделать с помощью простого и более управляемого кода.
Что нужно знать: http://www.troubleshooters.com/codecorn/littperl/perlreg.htm
Базы данных
Мы уже говорили о безопасности баз данных, но как обычно, информации в этой сфере не может быть слишком много. Давайте поговорим о самой распространенной базе — MySQL. Как и PHP, MySQL имеет очень плохую репутацию. В такой ситуации нет ничего удивительного — эта парочка является стандартной конфигурацией веб-сервера во всем мире. Не смотря на море негативных отзывов, MySQL справляется с решением всех стандартных потребностей. Проблема состоит в том, что многие преждевременно оптимизируют свои веб-проекты и выбирают NoSQL-решения, считая их более быстрыми или даже просто потому, что «крутые парни делают так». Излишне говорить, что такое решение на самом деле не оправдано в большинстве случаев. PHP позволяет работать с действительно эффективной базой данных. Если это MySQL, вы сэкономите много времени на которое затрачивается на разбор трудностей, связанных с конфигурацией. Поймите, вам не нужна какая-то особенная БД для веб-проекта. Экспериментируя вы, вероятно, потратите много времени на поиски. Зачем? Берите то, что уже хорошо изучено.
Чего следует избегать
Избегайте подключения к базе данных, когда запрос этого не требует. Когда ваш запрос в этом не нуждается, вы должны поддерживать постоянное соединение с базой данных посредством специальной функции (например, mysql_pconnect()), которая уменьшает время доступа. Избегайте выполнения множества запросов в пользу их консолидации. Каждый раз, когда вы отправляете запрос на сервер БД, ваша программа должна находиться в ожидании, пока данные возвращаются. Это хорошее решение, позволяющее избежать чрезвычайно сложных запросов, особенно если вам нужно планировать масштабируемость. SQL дает много поводов для раздумья, работайте над производительностью и увеличивайте ее!
О наследуемых библиотеках
Вы можете быть удивлены, тому, что я говорю об устаревшем функционале для MySQL. Взять хотя бы библиотеки абстракций БД — люди по-прежнему активно используют их. Но скорее всего, вы столкнетесь с проблемами, например, при отладке унаследованного кода или когда начнете работать с другими библиотеками. Я за сознательное отношение к данным, которому мешает использование устаревших библиотек. Опять же: используйте PDO, или все, что подходит вам, избегайте незащищенных mysql-вызовов.
Что нужно знать:
- MySQL, очевидно: http://dev.mysql.com/doc/refman/5.0/en/tutorial.html
- важное значение имеют индексы
Загрузка файлов
Загрузка файла — это акт приема файла из браузера в ваше веб-приложение. Как в случае со всеми вводимыми пользователем данными, вы должны быть готовы к худшему. Люди будут пытаться привести к сбою загрузки или загрузить исполняемые файлы на ваш сервер. Поэтому при приеме файла очень важно проверить его содержимое перед тем как оставить на хранение на сервере. Если злоумышленнику удается загрузить файл с PHP-кодом в один из каталогов, то игра закончена.
Чтобы избежать этого, вы должны убедиться, что, скажем, изображения, загруженные в ваше приложение, действительно являются изображением. Переменная $_FILES содержит информацию о MIME-типе файла. К сожалению, у вас нет возможности принимать во внимание эту информацию, потому что она предоставляется браузером пользователя и оттого может быть очень опасной. Вместо этого, вы должны получить фактический MIME-тип файла непосредственно из самого файла. В старые добрые времена вы могли бы использовать mime_content_type(), однако теперь эта функция устарела. Спасение приходит, откуда не ждали: GD-библиотека имеет функцию, которая, среди прочего, возвращает фактический MIME-тип файлов изображений — getimagesize(). Используйте ее, чтобы проверить загруженный файл и отвергнуть все, что не соответствует разрешенным MIME-типам файлов.
Что нужно знать:
- MIME-типы: http://en.wikipedia.org/wiki/MIME
- HTTP-методы: http://en.wikipedia.org/wiki/HTTP#Request_methods
Дополнительно: кэширование
На многих серверах вы будете иметь доступ к сервису под названием memcached. По сути это мини-серверный процесс, который позволяет очень быстро хранить и извлекать произвольные данные. Для хранения или извлечения пакета данных из кэша, ваше приложение должно подключиться к серверу memcached и дать ему ключ от объекта, в котором вы заинтересованы. Ключ может быть любой строкой. Имейте в виду, однако, что другие приложения, используя тот же ключ/значение хранения, поэтому выбирать ключи таким образом, чтобы это не привело к конфликтам. Помните, для хранения определенной пользовательской информации вам нужен определенный пользовательский ключ.
Как в случае с любым сервером, подключение к кэшу и получение кэшированных объектов требует временных затрат.
В отсутствии memcached-сервиса, вы можете запросто эмулировать его. Например, если ваше приложение должно сформировать отчет или большой кусок HTML-кода, который редко меняется, вы можете хранить эти данные в файловой системе. Просто укажите в приложении путь до TMP папки и храните кэшированные объекты там. Но в то время как срок хранения memcached-объектов истекает автоматически, вам придется позаботиться о жизненном цикле кэш-файлов самостоятельно.
Больше оптимизации
Поддержание быстрого времени отклика приложения это только начало. Существует гораздо больше вещей, которые необходимо проделать, чтобы ускорить загрузку страниц. Если вдаваться во все подробности, то обсуждение этой темы выходит за пределы статьи. Есть несколько книг, посвященных этим проблемам. Просто дам несколько советов, которые обычно не рекламируют:
Консолидация статических файлов
PHP делает этот процесс очень простым, объединяя в поток для передачи пользователю файлы различного содержания, в том числе JavaScript и CSS-файлы. Хорошее обслуживание JavaScript и CSS-файлов PHP-интерпретатором основано на двух моментах: во-первых, у вас есть полный контроль над HTTP-заголовками, необходимыми для настройки кэширования в браузере, — в большинстве случаев этот вариант не понадобится вам при обслуживании статических файлов веб-сервером напрямую. Во-вторых, вы можете объединить несколько файлов JS и CSS в один большой файл и отправить его.
Скажем, веб-приложению для функционирования необходима поддержка JQuery, 4 JQuery плагина, 2 пользовательских файла JS-кода и 4 CSS-файла. Браузер должен сделать 11 запросов, чтобы получить эти файлы. В то время как сами файлы могут быть небольшими, задержка между запросами будет сказываться на быстродействии. Что еще хуже, вы будете иметь по крайней мере 11 включений тегов внутри вашего HTML-документа при каждом запросе! Вы можете объединить эти файлы вручную в текстовом редакторе, но это усложнит их поддержку. Легче всего было бы скомпилировать два больших файла: JS и CSS.
Следите за выводом данных
Время от времени просматривайте код HTML, который выводится в результате работы вашего приложения. Удивительно, сколько лишней информации может появиться там. Обращайте внимание на каждый элемент и спрашивайте себя, точно ли на этом самом месте должен быть конкретно этот фрагмент кода. То же самое касается CSS и JavaScript файлов: с течением времени они имеют тенденцию накапливать мертвые куски кода, которые уже нигде не используются. Команда Unix «grep» станет вашим помощником в этом деле. С grep, вы можете осуществлять поиск на предмет вхождений строки во всем приложении. Он действительно поможет вам разобраться, используется ли данный кусок кода в настоящее время.
Комментарии
Можно много что еще сказать по всем этим пунктам. Радует, что раскрытого материала хватит хотя бы на то, чтобы указать интересующимся правильное направление. Скорее всего, закаленные в боях ветераны PHP не согласятся со всем вышеперечисленным или большинством из этого. Они имеют право на свою точку зрения. А вы? Решите, что имеет для вас смысл.
Удачи в программировании!