Загрузка и загрузка файлов, когда файлы хранятся за пределами веб-корня? (PHP)


Я создаю веб-приложение, которое по сути является доской объявлений о вакансиях - пользователи создают "задания/проекты", которые затем представляются в виде глобального списка "дел" для просмотра остальной частью компании.

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

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

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


Используя комбинацию распространенных методов загрузки и ссылки на книгу PHP (PHP и MySQL для динамических веб-сайтов - Ларри Уллман), я создал следующий PHP-скрипт:

[..]
// check if the uploads form has been submitted:
if($_SERVER['REQUEST_METHOD'] == 'POST') {

//check if the $_FILES global has been set:
if (isset($_FILES['upload'])) {

    //create a function to rewrite the $_FILES global (for readability):
    function reArrayFiles($file) {
        $file_ary = array();
        $file_count = count(array_filter($file['name']));
        $file_keys = array_keys($file);
        for ($i=0; $i<$file_count; $i++) {
            foreach ($file_keys as $key) {
                $file_ary[$i][$key] = $file[$key][$i];
            }
        }
        return $file_ary;
    }

    //create a variable to contain the returned data & call the function 
        //**Quick note: I thought simply stating 'reArrayFiles($_FILES['upload']);' would be enough, but I guess not
    $file_ary = reArrayFiles($_FILES['upload']);

    //establish an array of allowed MIME file types for the uploads:
    $allowed = array(
        'image/pjpeg', //.jpeg
        'image/jpeg',
        'image/JPG',
        'image/X-PNG', //.png
        'image/PNG',
        'image/png',
        'image/x-png',
        'image/gif', //.gif
        'application/pdf', //.pdf
        'application/msword', //.doc
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document', //.docx
        'application/vnd.ms-excel', //.xls
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', //.xlsx
        'text/csv', //.csv
        'text/plain', //.txt
        'text/rtf', //.rtf
    );

    //these are two arrays for containing statements and errors that occured for each individual file upload
    //so I can choose exactly where these "errors" print on page, rather than printing where the script as a whole is called
    $statement = array();
    $upload_error = array();

    //multi-file upload, so perform checks and actions on EACH file upload individually:
    foreach ($file_ary as $upload) {

        //validate the uploaded file's MIME type using finfo:
        $fileinfo = finfo_open(FILEINFO_MIME_TYPE); //open handle

        //read the file's MIME type (using magic btyes), then check if it is w/i the allowed file types array
        if ( in_array((finfo_file($fileinfo, $upload['tmp_name'])), $allowed) ) {

            //check the file's MIME type AGAIN, but this time using the rewritten $_FILES['type'] global
            //it may be redundant to check the file type twice, but I felt this was necessary because some files made it past the first conditional
            if ( in_array($upload['type'], $allowed) && ($upload['size'] < 26214400) ) {

                //set desired file structure to store files:
                //the tmp directory is one level outside my webroot
                //the '$job_data[0]' variable/value is the unique job_id of each project
                //here, it is used to create a folder for each project's uploads -- in order to keep them organized
                $structure = "../tmp/uploads/job_" . $job_data[0] . "/";

                //check if the folder exists:
                if (file_exists($structure) && is_dir($structure)) {

                    //if directory already exists, get file count: (files only - no directories or subdirectories)
                    $i = 0;
                    if (($handle = opendir($structure))) {
                        while (($file = readdir($handle)) !== false) {
                            if (!in_array($file, array('.','..')) && !is_dir($structure.$file)) 
                                $i++;
                        }
                        closedir($handle);
                        $file_count = $i;
                    }
                } else {
                    //directory does not exist, so create it
                    //files are NOT counted b/c new directories shouldn't have any files) -- '$file_count == 0'
                    mkdir($structure);
                }

                //if file count is less than 10, allow file upload:
                //this limits the project so it can only have a maximum of 10 attachments
                if ($file_count < 10) {

                    if (move_uploaded_file($upload['tmp_name'], "$structure{$upload['name']}")) {
                        $statement[] = '<p>The file has been uploaded!</p>';
                    } else {
                        $statement[] = '<p class="error">The file could not be transfered from its temporary location -- Possible file upload attack!</p>';
                    }

                } else if ($file_count >= 10) { 

                    //if there are already 10 or more attachments, DO NOT upload files, return statement/error
                    $statement[] = '<p class="error">Only 10 attachments are allowed per Project.</p>'; 
                } 

            //ELSE FOR 2ND FILE TYPE CHECK:
            } else {
                $statement[] = '<p class="error">Invalid basic file type.</p>';
            }

            //set an error msg to $upload_error array if rewritten $_FILES['error'] global is not 0
            //this section of code omitted; literally every upload script does this
            if ($upload['error'] > 0) {
                switch ($upload['error']) {
                    [...]
                }
            }

            //remove the temp file if it still exists after the move/upload
            if ( file_exists($upload['tmp_name']) && is_file($upload['tmp_name']) ) {
                unlink ($upload['tmp_name']);
            }

        //ELSE FOR 1ST FILE TYPE CHECK
        } else {
            $statement[] = '<p class="error">Invalid MIME file type.</p>';
        }

        //close the finfo module
        finfo_close($fileinfo);

    } //END OF FOREACH

} //END OF isset($_FILES['upload']) conditional

}//END OF $_SERVER['REQUEST_METHOD'] == 'POST' conditional

Мой HTML выглядит так:

<form enctype="multipart/form-data" action="edit-job.php" method="post">
<input type="hidden" name="MAX_FILE_SIZE" value="26214400"/>
<fieldset>
    <legend>Upload Project Files</legend>
    <input type="file" name="upload[]"/>
    <input type="file" name="upload[]"/>
    <input type="file" name="upload[]"/>
    <input type="file" name="upload[]"/>
    <input type="file" name="upload[]"/>
    <p>Max Upload Size = 25MB</p>
    <p><b>Supported file types:</b> .jpeg, .png, .gif, .pdf, .doc, .docx, .xls, .xlsx, .csv, .txt, .rtf</p>
</fieldset>
<input type="submit" name="submit" value="Edit Job"/>
</form>

Подводя итог, я представил PHP-скрипт для загрузки нескольких файлов (с проверкой) и сопровождающий HTML.

Мой метод НЕ использует базу данных MySQL, где таблица приравнивает идентификаторы проектов к связанным вложениям/файлам, как я видел, используют другие методы загрузки.

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

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


Несмотря на это, вот мои вопросы:

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

Сначала я попытался создать базовые ссылки на файлы, например:

<a href="../tmp/uploads/{unique_folder}/{file_name}" target="_blank">{file_name}</a>';

Но это, очевидно, не сработало из-за ограничений/внутренней безопасности. Затем я узнал, что лучше использовать отдельный"download.php " файл (поправьте меня, если я ошибаюсь), вот так:

'<a href="download.php?id=' . $job_data[0] . '&file_name=' . $file . '" target="_blank">' . $file . '</a>';

(передача переменных в отдельный файл .php)

Но что должен содержать этот файл.php? Я читал всевозможные вещи о функции заголовка php(), воссоздании файлов .pdf из оригиналов tmp и т. Д.

Я просто не могу понять всего этого...

Вот ссылка на то, о чем я говорю:

Http://web-development-blog.com/archives/php-download-file-script - похоже, вы используете php для доступ к файлам вместо того, чтобы разрешать это браузеру пользователя; может ли кто-нибудь проверить этот ресурс?

(2) Я делаю что-нибудь не так?

  • Должен ли я использовать базу данных MySQL для вложений (как я уже упоминал)?
  • Есть ли очевидные недостатки безопасности в моем сценарии загрузки?
  • Должен ли быть механизм панели загрузки для медленной загрузки? И т.д.

Я беспокоюсь о целостности моего веб-приложения в целом; Я не хочу, чтобы на меня влиял SQL инъекции/другие методы взлома.

Но более того, я хочу избавиться от любых плохих практик, которые могут возникнуть у меня как у начинающего разработчика.


Мы очень ценим ваши отзывы; дайте мне знать, если вам понадобится какая-либо дополнительная информация.

Author: CodyAE, 2016-09-14

1 answers

Относительно (1):

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

Сценарий, который вы упомянули в своем вопросе, вполне подходит, хотя я бы, вероятно, просто использовал fpassthru вместо цикла feof-fread-echo. Идея здесь, по сути, состоит в том, чтобы определить тип mime, добавить его в заголовки, а затем выгрузить содержимое в выходной поток.

Относительно (2):

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

Вы не проверяете upload_name, это вполне может быть ../../your webroot/index.php или что-то подобное. Мой совет состоял бы в том, чтобы хранить загруженные файлы как нечто невообразимое, как "Идентификатор файла", и хранить этот идентификатор с исходным именем файла в базе данных. Вероятно, вам также следует удалить все ведущие множественные точки, косые черты ("каталог") и тому подобное.

Загрузка брусья... ну, это, наверное, вкус.

 1
Author: Jakumi, 2016-09-14 15:42:18