文件上传和下载文件存储在webroot之外? (PHP)

时间:2016-09-14 07:16:27

标签: php html mysql file-upload download

我正在构建一个基本上是工作板的网络应用程序 - 用户创建“工作/项目”,然后将其作为全球“待办事项”列表呈现给公司其他人看。

我尝试实现的功能之一是“附件”功能,允许用户将文件作为每个项目数据的一部分上传。

这个想法是允许用户安全地上传文件,然后允许其他用户下载附件。

例如,如果我们为客户创建产品包装,那么能够将客户的徽标(.pdf或其他)作为项目数据的一部分附加,这样任何设计师都可以查看使用我们的工作板的项目可以下载该文件。

结合使用常见的上传技术和参考PHP书籍( PHP和MySQL for Dynamic Websites - 作者:Larry Ullman ),我构建了以下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数据库,其中表将项目ID与关联的附件/文件等同,正如我所见,其他上传方法使用。

它只是为webroot外部的公共位置中的每个项目的附件创建唯一的文件夹,因为这应该更安全。

此时,我承认这一切似乎都非常不正统,但在我不得不担心用户下载之前它一直运作良好!

无论如何,这是我的问题:

(1)如何允许用户使用我的结构(在webroot之外)下载文件?

我最初尝试创建文件的基本链接,如下所示:

<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的header()函数的各种事情,从tmp原件中重新创建.pdf文件等。

我无法理解这一切......

这里是我正在谈论的链接:

http://web-development-blog.com/archives/php-download-file-script   - 听起来你使用php访问文件而不是允许用户的浏览器这样做;任何人都可以验证这个资源吗?

(2)我做错了什么?

  • 我应该使用MySQL数据库作为附件(就像我提到的那样)吗?
  • 我的上传脚本中是否存在明显的安全漏洞?
  • 是否应该有一个加载条机制来缓慢上传?等

我担心整个网络应用程序的完整性;我不想受SQL注入/其他黑客攻击方法的影响。

但除此之外,我想清除我作为新手开发者可能遇到的任何不良做法。

非常感谢您的反馈;如果您需要任何其他信息,请与我们联系。

1 个答案:

答案 0 :(得分:1)

关于(1):

理论是:您将文件存储在webroot之外的某个位置,以防止直接访问并阻止在服务器端执行。当用户提供正确的参数时,您必须能够找到它。 (但是,您应该小心,如何将文件呈现给用户?数据库在这里可能会有所帮助)如果担心安全性,则必须确保用户无法访问他无权访问的文件(当他有正确的下载链接但没有权限时,例如,因为到目前为止,你的下载链接似乎并不是很神秘。)

你在问题​​中提到的脚本非常好,虽然我可能只使用fpassthru而不是feof-fread-echo-loop。这里的想法主要是找出mime类型,将其添加到头文件中,然后将内容转储到输出流中。

关于(2):

使用数据库尤其是使用预处理语句是非常安全的,并提供了一些额外的可能性。 (比如在附件中添加一些注释,时间戳,文件大小,重新排序......)

您不检查upload_name,这可能是../../your webroot/index.php或类似的东西。我的建议是将上传的文件存储为缺乏想象力的文件“file_ID”,并将该id与原始文件名存储在数据库中。您可能还应删除任何前导多个点,斜杠(“目录”)和类似的。

加载吧......我觉得这很有味道。

相关问题