Pull to refresh

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

Reading time7 min
Views28K
Original author: Alla Bezroutchko
Это вторая часть перевода. Начинать прочтение лучше с первой.

Итак, после применения описанных в первой части методов, мы можем прекратить волноваться? К сожалению, нет. То, какие расширения файла будут переданы транслятору 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 и расширение файла вообще доверять нельзя. Их очень просто подделать. Более того, список исполняемых расширений зависит целиком от веб-сервера, и нет никакой гарантии что он со временем не изменится.

Производительность очень важна, но совершенно невозможно обеспечить безопасную загрузку файлов на сервер без ущерба для неё.
Tags:
Hubs:
+55
Comments31

Articles

Change theme settings