Безопасность PHP: как запретить пользователям загружать исполняемые файлы PHP

PHP

file

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

Около месяца назад я былredditпоследнее чтениеОбнаружение уязвимости загрузки PHP, поэтому решил написать статью. ПользовательdarpernterЗадал сложный вопрос:

Сможет ли злоумышленник запустить свой php-скрипт, даже если я переименовал его в «helloworld.txt»?

Верхний ответ:

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

Извините, правильный ответ на вопрос не так.Хотя не все ответы выше неверны, они явно не исчерпывающие. Удивительно, но большинство ответов были очень похожи.

Я хочу ясно объяснить эту проблему. Итак, то, о чем я собирался рассказать, стало немного больше, и я решил сделать это больше.

проблема

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

Начнем с того, как выполняется файл php. Предполагая сервер с php-средой, у него обычно есть два способа внешнего выполнения php-файлов. Один из них — запросить файл напрямую с URL-адресом, напримерhttp://example.com/somefile.php. Второй сейчас обычно используется php и перенаправляет все запросы наindex.php, и каким-то образом импортировать другие файлы в этот файл. Итак, есть два способа запустить код из файла php: выполнить файл или использоватьinclude/include_once/require/require_onceМетод вводит другие файлы, которые необходимо запустить.

На самом деле есть третий способ:eval()функция. Он может выполнять входящую строку как php-код. Эта функция используется в большинстве систем CMS для выполнения кода, хранящегося в базе данных.eval()Функции очень опасны, но если вы их используете, это обычно означает, что вы уверены, что делаете что-то опасное, и что у вас нет другого выбора. На самом деле у eval() есть свое предназначение, и в определенных ситуациях оно очень полезно. Но если вы новичок, я не рекомендую вам использовать его. посмотри пожалуйстаЭта статья на OWASP. Я много писал об этом.

Итак, есть два способа выполнить код в файле: выполнить его напрямую или импортировать в исполняемый файл. Так как же этого избежать?

Решение?

Как мы можем узнать, что файл содержит php-код? Глядя на расширение, если оно начинается с.phpокончание вродеsomefile.phpМы просто предположили, что в нем есть php-код.

если естьsomefile.phpфайл, затем зайдите в браузереhttp://example.com/somefile.php, файл будет выполнен и выведен в браузер.

Но что, если я переименую этот файл? если я переименую его вsomefile.txtилиsomefile.jpgШерстяная ткань? что я получу? Я получу его содержимое. Он не будет выполнен. Он будет отправлен непосредственно с жесткого диска (или кэша).

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

Я уверен, что вы заметили вопросительный знак, который я поставил после слова «обходной путь». Этот вопросительный знак имеет смысл. Отдельный файл php практически не встречается в URL-адресах большинства веб-сайтов в наши дни. А даже если и есть, то он искусственно подделан, потому что URL-адрес должен иметь.phpдля обратной совместимости со старыми URL-адресами.

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

Как я могу предотвратить внедрение загруженных пользователем файлов?

* Можно ли переименовать имя файла? --- *Нет, это невозможно!

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

Обычно сервер настроен на выполнение.phpфайл и вернуть результат выполнения на выход. если вы просите фотографию.jpg ---  будут возвращены с диска как есть. Что произойдет, если вы попросите сервер каким-либо образом запустить изображение в формате jpeg? Будет сервер это делать или нет?

file

Изображение предоставлено: Эхо / Культура / Getty Images

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

Что нужно для выполнения кода PHP из файла?

Есть как минимум две ситуации, когда PHP может выполнять код:

  1. код между<?php и ?>между маркерами
  2. код между<?= и ?>между маркерами

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

Вот вам картинка:

file

Картинка в порядке

Теперь чисто. Но вы, наверное, знаете, что формат JPEG позволяет добавлять к файлу некоторые комментарии. Например, модель камеры или координатный адрес, где была сделана фотография. Если мы попытаемся поместить внутрь некоторый PHP-код и попробуемinclude или requireШерстяная ткань? Давайте взглянем!

Вопрос 1

Загрузите этот образ на жесткий диск. Или вы можете получить изображение в формате JPEG самостоятельно. Неважно, какой формат файла вы используете. Я рекомендую файл JPEG для презентации, главным образом потому, что это изображение и в нем легко редактировать текст. Я использую ноутбук с Windows, и в настоящее время у меня нет под рукой ноутбука Apple или Linux (или другой системы на базе UNIX). Так что через какое-то время я опубликую скриншот этой ОС. Но я уверен, что вы тоже можете это сделать.

Создайте файл со следующим кодом PHP:

<h1>Problem?</h1>
<img src="troll-face.jpg">
<?php
include "./troll-face.jpg";
  1. Сохраните изображение с именемtroll-face.jpg
  2. Поместите изображения и файлы сценариев php в одну папку
  3. Откройте браузер и запросите этот файл php

Если вы назвали свой php-файл какindex.phpи поместите его в корневой каталог файла или в любой каталог файлов в каталоге вашего веб-сайта.

Если вы правильно выполните вышеуказанные шаги, вы увидите этот экран:

file

Пока это нормально. PHP-код не отображается и PHP-код не выполняется.

Теперь добавим вопрос:

  1. Откройте диалог свойств файла или запустите какое-нибудь приложение, позволяющее редактировать информацию EXIF.
  2. Перейдите на вкладку «Подробности» или иным образом отредактируйте информацию.
  3. Прокрутите вниз до параметра камеры
  4. Скопируйте следующий код после поля «производитель камеры»:
<?php echo "<h2>Yep, a problem!</h2>"; phpinfo(); ?>

file

обновите страницу!

file

Явно что-то пошло не так!

Вы видели это изображение на странице. Такое же изображение существует и в PHP-коде страницы. Также выполняется код для изображения.

Что нам делать?!!1

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

Посмотрите внимательно на пример ниже.

Окончательный ответ?

Если кто-то видит, что я где-то ошибаюсь - поправьте меня, это серьезная проблема.

PHP — это язык сценариев. Вам всегда нужно ссылаться на некоторые файлы с динамически комбинируемыми путями. Поэтому, чтобы защитить свой сервер, вы должны проверять пути и не допускать путаницы между файлами вашего сайта и файлами, загруженными или созданными пользователями. Если файлы пользователя отделены от файлов приложения, вы можете проверить путь к файлу перед загрузкой или созданием файла с его использованием. Если он находится в папке, разрешенной вашим скриптом приложения, он может использовать include_once или require или require_once для извлечения этого файла. Если нет -- то не вводите.

Как проверить? это очень просто. вам просто нужно поставить$folder(файл) и файл, который позволяет программам импортировать ($file), чтобы сравнить пути к папкам.

// 不好的例子,不要用!
if (substr($file, 0, strlen($folder)) === $folder) {
  include $file;
}

если $folderПуть хранения/path/to/folder и $fileПуть хранения/path/to/folder/and/file, то используем в кодеsubstr()Функция превращает их пути в отрицательные строки для суждения, если файлы находятся в разных папках --- строки не будут равны. Верно и обратное.

В приведенном выше коде есть две важные проблемы. еслиfileпуть/path/to/folderABC/and/file, и видно, что файл тоже не в той папке, которую разрешено импортировать. Этого можно избежать, добавив косую черту к обоим путям. Неважно, что здесь мы добавляем косую черту к пути к файлу, так как нам нужно сравнить только две строки.

Например: еслиfolderпуть/path/to/folderиfileпуть/path/to/folder/and/file, то изfileИзвлечь иfolderимеют одинаковое количество символов, то$ folderбудет/path/to/folder.

Другой примерfolderпуть/path/to/folderиfileпуть/path/to/folderABC/and/file, то изfileизвлечь изfolderимеют одинаковое количество символов и$folderто же самое, и снова будет/path/to/folder, это все неправильно, это не тот результат, которого мы ожидаем.

Таким образом, в/path/to/folder/После добавления косой черты с помощью/path/to/folder/and/fileэкстракционная часть/path/to/folder/То же безопасно.

если/path/to/folder/и/path/to/folderABC/and/fileэкстракционная часть/ path/to/folderA, очевидно, две строки не совпадают.

Это то, что мы ожидаем. Но есть еще одна проблема. Это не очевидно. Я уверен, что если бы я спросил вас, вы бы увидели здесь катастрофическую уязвимость — вы бы не догадались, где она. Возможно, вы использовали эту штуку в опыте, может быть, даже сегодня. Теперь вы увидите, насколько незаметна и очевидна уязвимость. Посмотрите вниз.

/../

Представьте себе очень распространенный сценарий.

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

/path/to/folder/../../../../../../../another/path/from/root/

Пример. Пользователь инициирует запрос, и ваш сценарий включает файл на основе пути ввода пользователя, например:

include $folder . "/" . $_GET['some']; // or $_POST, or whatever

Вы в большой беде. Однажды пользователь отправляет../../../../../../etc/.passwdЭта или любая другая просьба, просто плачь.

В противном случае. Если кто-то скажет вашему сценарию загрузить нужный ему файл, вы мертвы. Он не должен просто появляться в пользовательских файлах. Это может быть какой-то плагин для вашей CMS или ваши собственные файлы (не доверяйте никому), или даже баг в логике приложения и т.д.

или

Пользователь может загрузить файл с именемfile.phpфайл, вы поместите его в определенную папку, как и любой другой пользовательский файл:

move_uploaded_file($filename, $folder . '/' . $filename);

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

Но http-запрос позволяет пользователю отправлять любые символы. Поэтому, если кто-то подделал запрос на создание../../../../../../var/www/yoursite.com/index.phpфайл --- эта строка кода перезапишет вашindex.phpфайл, еслиindex.phpпо указанному выше пути.

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

Решения и ответы

Чтобы решить эту проблему, в PHP есть несколько специальных методов функций, которые можно использовать в этой ситуации.

basename()

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

realpath()

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

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

мой код

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

<?php
/**
 * Example for the article at medium.com
 * Created by Igor Data.
 * User: igordata
 * Date: 2017-01-23
 * @link https://medium.com/@igordata/php-running-jpg-as-php-or-how-to-prevent-execution-of-user-uploaded-files-6ff021897389 Read the article
 */
/**
 * 检查某个路径是否在指定文件夹内。若为真,返回此路径,否则返回 false。
 * @param String $path 被检查的路径
 * @param String $folder 文件夹的路径,$path 必须在此文件夹内
 * @return bool|string 失败返回 false,成功返回 $path
 *
 */
function checkPathIsInFolder($path, $folder) {
    if ($path === '' OR $path === null OR $path === false OR $folder === '' OR $folder === null OR $folder === false) {
      /* 不能使用 empty() 因为有可能像 "0" 这样的字符串也是有效的路径 */
        return false;
    }
    $folderRealpath = realpath($folder);
    $pathRealpath = realpath($path);
    if ($pathRealpath === false OR $folderRealpath === false) {
        // Some of paths is empty
        return false;
    }
    $folderRealpath = rtrim($folderRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    $pathRealpath = rtrim($pathRealpath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
    if (strlen($pathRealpath) < strlen($folderRealpath)) {
        // 文件路径比文件夹路径短,那么这个文件不可能在此文件夹内。
        return false;
    }
    if (substr($pathRealpath, 0, strlen($folderRealpath)) !== $folderRealpath) {
        // 文件夹的路径不等于它必须位于的文件夹的路径。
        return false;
    }
    // OK
    return $path;
}

Вывод.

  • Пользовательский ввод должен быть отфильтрован, и имена файлов также являются частью пользовательского ввода, поэтому обязательно проверяйте имена файлов. Не забудьте использовать basename() .
  • Всегда проверяйте путь, по которому вы хотите хранить пользовательские файлы, никогда не смешивайте этот путь с каталогом приложения. Путь к файлу должен состоять из строкового пути к папке иbasename($filename)сочинение. Перед записью файла обязательно проверьте окончательный путь к составленному файлу.
  • Прежде чем ссылаться на файл, необходимо проверить путь, и он строго проверяется.
  • Не забудьте использовать некоторые специальные функции, потому что вы можете не знать об определенных слабостях или уязвимостях.
  • И, очевидно, это не имеет ничего общего с суффиксами файлов или mime-типами. JPEG допускает существование строк в файлах, поэтому действительное изображение JPEG также может содержать действительные скрипты PHP.

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

Конечно, не бойтесь, на самом деле это проще, чем кажется. Просто помните «не доверяйте пользователю» и «есть возможность это исправить».

Перенесено из сообщества разработчиков PHP/LaravelПотяните Ravel-China.org/topics/1962…