Безопасная загрузка изображений на сервер. Часть вторая

http://www.scanit.be/uploads/php-file-upload.pdf
  • Перевод
Это вторая часть перевода. Начинать прочтение лучше с первой.

Итак, после применения описанных в первой части методов, мы можем прекратить волноваться? К сожалению, нет. То, какие расширения файла будут переданы транслятору PHP, будет зависеть от конфигурации сервера. Разработчик часто не знает и не контролирует конфигурацию веб-сервера. Мы видели веб-серверы, с такой конфигурацией, что файлы .html и .js выполнялись как php. Некоторые веб-приложения могут потребовать, чтобы файлы .gif или .jpeg интерпретировались PHP (это часто случается, когда изображения, например графы и диаграммы, динамически строятся на сервере самим PHP).

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

Если у вас веб-сервер на основе Microsoft IIS, то надо иметь в виду еще несколько моментов. В отличие от Apache, сервер Microsoft IIS поддерживает выполнение «PUT»-запросов, которые позволят пользователям загружать файлы непосредственно, минуя PHP. PUT-запросы могут быть использованы для загрузки файлов на сервер, если системные права позволяют это сделать IIS (он запущен как IUSR_MACHINENAME). Это можно настроить с помощью Services Manager:

image

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

Косвенный доступ к загруженным файлам

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

Рассмотрим следующий пример (upload5.php):

<?php
 $uploaddir = '/var/spool/uploads/'; # Outside of web root
 $uploadfile = $uploaddir . basename($_FILES['userfile']['name']);

 if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
   echo "File is valid, and was successfully uploaded.\n";
 } else {
   echo "File uploading failed.\n";
 }
?>

* This source code was highlighted with Source Code Highlighter.

Пользователи не могут просто обратиться к /uploads/, чтобы загрузить файлы, и мы должны обеспечить дополнительную возможность этого (view5.php):

<?php
 $uploaddir = '/var/spool/uploads/';
 $name = $_GET['name'];
 readfile($uploaddir.$name);
?>


* This source code was highlighted with Source Code Highlighter.


Код view5.php обладает серьезной уязвимостью. Злоумышленник может использовать этот код, чтобы прочитать любой файл, который может быть прочитан на уровне прав веб-сервера. Например, если вызвать
www.example.com/view5.php?name=../../../etc/passwd, то возможно получится прочитать файл с паролями.

Этот баг может быть пофиксен с помощью функции результата dirname(realpath()), которая возвращает реальный путь к файлу. Таким образом, если этот путь некорректен, то не показываем файл. Аналогично с basename() – получаем только имя файла, которое уже присоединяем к корректному директорию загрузки. – Прим. переводчика.

Использование локальных include (Local file inclusion attacks)

Это одна из самых страшных дыр безопасности на сайтах. Она настолько хорошо известна, что в действительности уже почти не проявляется. Но, как говорится – «повторение – мать учения». – Прим. переводчика.

Прошлый пример хранит загруженные файлы за пределами корня, где к ним нельзя получить прямой доступ и выполнить. Хотя это и безопасно, но у злоумышленника может быть шанс использовать это в своих интересах, если в коде присутствует другая уязвимость – использование include. Предположим, что у нас есть некоторая другая страница, которая содержит следующий код (local_include.php):

<?php
// ... some code here

 if(isset($_COOKIE['lang'])) {
   $lang = $_COOKIE['lang'];
 } elseif (isset($_GET['lang'])) {
   $lang = $_GET['lang'];
 } else {
  $lang = 'english';
 }

 include("language/$lang.php");

// ... some more code here
?>

* This source code was highlighted with Source Code Highlighter.
Это общая часть кода, которая обычно имеет место в многоязычных веб-приложениях. Подобный код может обеспечить различный include файлов в зависимости от пользовательских предпочтений.

Код имеет от include-уязвимость. Нападавший может заставить эту страницу включать любой файл из файловой системы, например:

image

Этот запрос заставляет local_include.php включать и выполнить «language/../../../../../../../../tmp/phpinfo», который является просто/tmp/phpinfo. Нападавший может выполнить только файлы, которые уже находятся на стороне сервера, таким образом его возможности ограничены.

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

Раньше существовала настройка php, которая позволяла инклудить файлы по url, таким образом можно было выполнить код, который мог и не находиться на сервере. В последних версиях php такой возможности в принципе нет из соображений безопасности. – Прим. переводчика


В итоге

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

Рассмотрим пример (upload6.php):

<?php
 require_once 'DB.php'; // We are using PEAR::DB module
 $uploaddir = '/var/spool/uploads/'; // Outside of web root
 $uploadfile = tempnam($uploaddir, "upload_");

 if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
   // Saving information about this file in the DB
   $db =& DB::connect("mysql://username:password@localhost/database");

   if(PEAR::isError($db)) {
     unlink($uploadfile);
     die "Error connecting to the database";
   }

   $res = $db->query("INSERT INTO uploads SET name=?, original_name=?, mime_type=?",
         array(basename($uploadfile,
         basename($_FILES['userfile']['name']),
         $_FILES['userfile']['type']));

   if(PEAR::isError($res)) {
     unlink($uploadfile);
     die "Error saving data to the database. The file was not uploaded";
   }

   $id = $db->getOne('SELECT LAST_INSERT_ID() FROM uploads'); // MySQL specific

   echo "File is valid, and was successfully uploaded. You can view it <a
      href=\"view6.php?id=$id\">here</a>\n"
;
 } else {
   echo "File uploading failed.\n";
 }
?>

* This source code was highlighted with Source Code Highlighter.


Просмотр загруженного файла (view6.php):

<?php
 require_once 'DB.php';
 $uploaddir = '/var/spool/uploads/';
 $id = $_GET['id'];

 if(!is_numeric($id)) {
  die("File id must be numeric");
 }

 $db =& DB::connect("mysql://root@localhost/db");
 
 if(PEAR::isError($db)) {
  die("Error connecting to the database");
 }

 $file = $db->getRow('SELECT name, mime_type FROM uploads WHERE id=?',
     array($id), DB_FETCHMODE_ASSOC);

 if(PEAR::isError($file)) {
   die("Error fetching data from the database");
 }

 if(is_null($file) || count($file)==0) {
   die("File not found");
 }

 header("Content-Type: " . $file['mime_type']);
 readfile($uploaddir.$file['name']);
?>

* This source code was highlighted with Source Code Highlighter.


Теперь загруженные файлы нельзя непосредственно выполнить (потому что они сохранены за пределами корня). Они не могут использоваться в include-уязвимостях, потому что у нападавшего нет возможности узнать имя загруженного файла в файловой системе на сервере. Есть некоторая проблема с «пересечением» файлов, потому что файлы привязаны к числовому индексу, а не к его имени. Также хотелось бы указать на использование PEAR::DB для SQL-запросов. В нашем SQL используются вопросительные знаки как места для переменных запроса. Когда данные, полученные от пользователя, передаются в запрос, их типы автоматически расставляются, предотвращая проблемы SQL-инъекций.

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

Другие проблемы

Существует еще много вещей, на которые стоит обратить внимание при разработке систем с загрузкой файлов на сервер, а именно:
  1. Отказ в обслуживании. Пользователь может загрузить несколько больших файлов, тем самым заняв все свободное место на сервере. Это решается, выставлением ограничения на размер загружаемого файла и на количество загружаемых файлов от одного пользователя в день.
  2. Производительность. В последнем примере при частых запросах на показ файла просмотр может быть узким местом. Если сервер сильно нагружен, то рекомендуется использовать еще один, предназначенный только для хранения статичного контента, на котором не может быть выполнен php-код. Еще одним способом повысить производительность является использование кэширующего прокси-сервера, который предотвращает повторную обработку статического контента на стороне сервера выдавая соответствующие заголовки.
  3. Управление доступом. Во всех примерах выше мы предполагали, что любой пользователь может просмотреть любой загруженный файл. Однако, может потребоваться, что бы только тот пользователь который загрузил файл мог просмотреть его. В этом случае при загрузке должна сохраняться информация о владельце файла. При просмотре файла должна быть соответствующая проверка.

Заключение

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

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

Другой важной мерой безопасности – не хранить файлы на сервере под оригинальными именами файлов. Это предотвратит возможности Include-уязвимостей, даже если они есть, а так же сделает любую манипуляцию с именами файлов для злоумышленника невозможной.

Проверка формата изображения через PHP не дает никакой гарантии, что данный файл не может быть выполнен как php-скрипт. Можно создать корректное изображение, которое в тоже время будет являться выполнимым php-скриптом.

Приходящим от клиента данным, таким как Content-Type и расширение файла вообще доверять нельзя. Их очень просто подделать. Более того, список исполняемых расширений зависит целиком от веб-сервера, и нет никакой гарантии что он со временем не изменится.

Производительность очень важна, но совершенно невозможно обеспечить безопасную загрузку файлов на сервер без ущерба для неё.
Метки:
Поделиться публикацией
Похожие публикации
Комментарии 31
  • 0
    Есть еще один способ защиты при загрузке изображений — использование nginx без fastcgi модуля для отдачи изображений из папки загрузки :)
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        А еще лучше, помимо X-Accel-Redirect использовать mod_upload.
        X-Accel-Redirect позволит считать статистику при скачивании (ну или другие действия, по вкусу), при этом обезопасить себя от DDOS-ов путем скачивания больших файлов.
        mod_upload наоборот, для того что бы обезопасить себя от DDOS-ов путем закачивания файлов.

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

        P.S. enartemy, благодарю за инвайт :)
      • 0
        Скорее это способ решить часть уязвимостей, затронутых в статье
        • –6
          +1 :)
          • +3
            минусы сносят хабрасилу гораздо больше, чем плюсы её нарасчивают, обидно
            • –1
              -1 :)
            • 0
              при этом нужно, чтоб папка загрузки не была доступна по http с PHPшного сервера, а то бестолку
            • 0
              Интерестные статьи, спасибо. Но я, к сожалению, пользуюсь Zend_File.
              Хотя, думаю, проверю его работу по вашем материалам. Еще раз спасибо!
              • 0
                Почему «к сожалению»? Механизм Zend_File очень удобен, поддержка фильтров и валидаторов позволяет построить все, о чем говорил автор.
              • 0
                Объясните мне пожалуйста, почему нельзя создать в директории /uploads/ файл .htaccess, где прописать -ExecCGI? Насколько я понимаю, это запретит выполнение php скриптов в данной папке… Или я не прав?
                • 0
                  при прямом обращению к файлу — да. Но не от include
                  • 0
                    Но почему тогда я никогда не видел подобного совета по безопасности вообще нигде? Ведь если скрипт заведомо не использует в include переменные, то этого должно хватить для полного обеспечения безопасности? Причем вроде бы без потери производительности?
                  • 0
                    Абсолютно правы, можно и так. Единственное, мы имеем открытый досуп к любым файлам из этой директории. И еще, я не уверен что хоть и нельзя выполнить файл, его также нельзя будет приинклудить.

                    Вощем, да, так можно — но надо еще в куче всего удостовериться.
                    • 0
                      Но ведь это такое простое решение :) Гораздо проще чем все эти проверки… Да, заинклудить файл можно будет, но если скрипт не использует переменных в инклудах? А открытый доступ что так что так есть — если уж мы отдаем файлы из этой директории. Да и в любом случае, подобный .htaccess явно повысит безопасность системы. Мне просто интересно, почему об этом никто не говорит? Я не верю, что я додумался до этого первым.
                      • 0
                        Да не, так конечно можно и нужно. Только это все равно не понацея от всего. В частности, это не решает проблему разделения доступа к файлу. Вообще никак.
                  • –1
                    только один вопрос, а как вам удалось вторую статью написать меньше чем за минуту? о_О
                    • 0
                      Как, вы ничего не слышали о том, что статьи можно сначала написать в ворде? :-)) А потом при написании статьи вы никогда не видели кнопочки «В черновики»? Она там есть, поверте… :-)
                      • 0
                        На самом деле это было некое подобие шутки. А мне тут начинают описывать сам тех процесс. Я думал НЛО написало вторую статью, пока автор писал первую.

                        В общем не важно…
                      • –1
                        написано же, «перевод»…
                        • –1
                          «перевод за минуту». гениально!
                      • 0
                        Слово «безлопастную» в конце неимоверно порадовало.
                        • 0
                          «Это вторая часть первода». Исправьте ошибку, пожалуйста.
                          • 0
                            В статье юзается DB модуль pear. почему бы просто не воспользоваться тем же HTTP_upload модулем и упростить себе задачу? Там можно вписать и ограничения расширений и обьёма файла, и сгенерировать новое имя для файла при загрузке
                            • 0
                              Это уже к Alla Bezroutchko :-) Тут вообще много чего можно упростить, но все-таки перевод должен оставаться перводом.

                              PS: Знаете как электронный переводчик перевел PEAR::DB? — ГРУША:: ДЕЦИБЕЛОВ :-)))
                            • +1
                              Например, если вызвать
                              www.example.com/view5.php?name=../../../etc/passwd, то возможно получится прочитать файл с паролями.

                              Пароли (вернее их хэши) уже давно в этом файле не хранятся.
                              • 0
                                Статья 2007-го года. За год все изменилось?
                                • +1
                                  Статья 2007-го года. За год все изменилось?

                                  en.wikipedia.org/wiki/Shadow_password
                                  Password shadowing first appeared in UNIX systems with the development of System V Release 3.2 in 1988 and BSD4.3 Reno in 1990. Vendors which had performed ports from earlier UNIX releases did not include the new password shadowing features, leaving users of those systems exposed to password file attacks.

                                  In 1987 the author of the original Shadow Password Suite, Julie Haugh, experienced a computer break-in and wrote the initial release of the Shadow Suite containing just the login, passwd and su commands. The original release, written for the SCO Xenix operating system, quickly got ported to other platforms. The Shadow Suite was ported to Linux in 1992 one year later from Linux announcement and became a part of many early distributions.


                                  P.S. Побольше читайте про Linux/UNIX, а не зацикливаейтесь на подобного рода статьях :)
                              • +1
                                не всегда можно сохранить файлы за пределами корня
                                • –5
                                  хороший профи сломает любой сайт, на который ему укажут… тут уж, извиняйте, ничего не поделаешь… причем часто, как я понимаю, проще сервак ломануть, а потом уже сам сайт… так что это все-- защита от студентов…

                                  как то раз присутствовал на теоретическом взломе-- это что-то… ломать-- не строить…
                                  • 0
                                    Спасибо за материал. Для себя решил
                                    1 сохранять файлы на отдельный сервер, где не будет выполняться php
                                    2 менять имя файла
                                    3 проверять размер файла (кстати как это лучше сделать?)
                                    4 ну и не использовать переменные в инклудах :)

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.