如何使用PHP安全地上传文件?

| 选择喜欢的代码风格  

PHP 安全上传文件 - 目录遍历


为避免目录遍历(也称为路径遍历)攻击,请使用 basename() 如下 upload.php 所示,甚至更好,请像在下一步中一样完全重命名文件。

<?php
foreach ($_FILES['pictures']['error'] as $key => $error) {
    if ($error == UPLOAD_ERR_OK) {
        $tmpName = $_FILES['pictures']['tmp_name'][$key];
        // basename() may prevent directory traversal attacks, but further
        // validations are required
        $name = basename($_FILES['pictures']['name'][$key]);
        move_uploaded_file($tmpName, "/var/www/project/uploads/$name");
    }
}

PHP 安全上传文件 - 重命名上传的文件


重命名上载的文件可以避免在上载目标中出现重复的名称,还有助于防止目录遍历攻击。如果需要保留原始文件名,则可以将其保存在数据库中以备将来使用。例如,使用 microtime() 和一个随机数重命名文件:

<?php
$uploadedName = $_FILES['upload']['name'];
$ext = strtolower(substr($uploadedName, strripos($uploadedName, '.')+1));

$filename = round(microtime(true)).mt_rand().'.'.$ext;

您还可以使用像hash_file()和这样的哈希函数sha1_file()来构建文件名。当不同的用户上载相同的文件时,此方法可以节省一些存储空间。

<?php
$uploadedName = $_FILES['upload']['name'];
$ext = strtolower(substr($uploadedName, strripos($uploadedName, '.')+1));

$filename = hash_file('sha256', $uploadedName) . '.' . $ext;

PHP 安全上传文件 - 检查文件类型


不用依赖文件扩展名,可以使用 finfo_file() 获得文件的 mime类型,这样也相对更安全,但需要安装 php_fileinfo 的扩展:

<?php

$finfo = finfo_open(FILEINFO_MIME_TYPE); // return mime-type extension
echo finfo_file($finfo, $filename);
finfo_close($finfo);

对于图像,使用此 getimagesize() 功能比较可靠,但仍然不够好:

<?php
$size = @getimagesize($filename);
if (empty($size) || ($size[0] === 0) || ($size[1] === 0)) {
    throw new \Exception('Image size is not set.');
}

PHP 安全上传文件 - 检查文件大小


检查上载的文件大小对于不使服务器过载文件很重要。检查上传的文件大小时,有几个主要级别需要研究。

upload_max_filesize INI 指令

最重要的是限制 php.ini 文件中的 upload_max_filesizeandpost_max_size ini 指令。这样可以防止服务器磁盘大小在服务器上过载。 upload_max_filesize 到达后,它将立即停止上传,并将 UPLOAD_ERR_INI_SIZE 错误代码设置为 $_FILES['key']['error']。如果 post_max_size 已到达 $_POST$_FILES 则将为空。

检查代码中的上传文件大小

上面的第二个最重要的一点是还要使用 filesize($_FILES['key']['tmp_name]) 功能或来检查应用程序代码中上传的文件大小 $_FILES['files']['size']。就上传文件而言,两者同等有效。

从以下位置限制或检查上传文件的大小 $_FILES['key']['size']

if ($_FILES['pictures']['size'] > 1000000) {
    throw new Exception('Exceeded file size limit.');
}

客户端表单验证(HTML5或JavaScript)

在客户端检查上传的文件大小是可选的,并不安全,但这可以改善用户体验。

$uploadedName = $_FILES['upload']['name'];
$ext = strtolower(substr($uploadedName, strripos($uploadedName, '.')+1));

$filename = hash_file('sha256', $uploadedName) . '.' . $ext;

MAX_FILE_SIZE 隐藏字段

然后,还进行了其他 PHP 特定的可选检查,即使用 PHP 可以使用的 HTML 形式使用带有名称 MAX_FILE_SIZE(或 max_file_size 不区分大小写)的特殊隐藏字段。但是,它可以与恶意客户端以与客户端验证相同的方式进行欺骗,因此它不可靠。在上传非常大的文件(例如视频)的情况下,这更多是用户体验的改善。

例如,以下格式会将文件大小限制为 1MB 或 1048576 字节(1*1024*1024):

<form method="post" enctype="multipart/form-data" action="upload.php">
    <input type="hidden" name="max_file_size" value="2097152">
    File: <input type="file" name="pictures[]" multiple="true">
    <input type="submit">
</form>

max_file_size 隐藏字段需要添加的文件输入字段之前在 PHP 端有效。

它通过以下方式限制了上传过程:

  • PHP 首先检查当前上传的字节是否大于 upload_max_filesize ini指令。
  • 如果没有,它还会检查该 max_file_size 字段是否已定义以及当前上传的字节是否大于该字段。如果是,它将中断上传过程,并在中设置 UPLOAD_ERR_FORM_SIZE 错误代码 $_FILES['key']['error']。这样,用户无需等待文件的 100% 上载,而是只上载了 max_file_size 字段值字节,并且应用程序停止了上载过程。

PHP 安全上传文件 - 将上传内容存储到私人位置


最好将上传的文件 https://commandnotfound.cn/uploads 保存到公共不可访问的文件夹中,而不是将上传的文件保存到位于的公共位置 。为了传递这些文件,使用了所谓的代理脚本。

PHP 安全上传文件 - 客户端验证


为了获得更好的用户体验,HTML 提供了accept 属性,以通过 HTML 中 的扩展名或 mime-type 限制文件类型,因此用户可以即时查看验证错误,并在浏览器中仅选择允许的文件类型。但是, 在撰写本文时,浏览器支持受到限制。请记住,黑客可以轻松绕过客户端验证。上面说明的服务器端验证步骤是要使用的更重要的验证形式。

PHP 安全上传文件 - 完整的例子


让我们考虑以上所有内容,并看一个非常简单的示例:

<?php

// Check if we've uploaded a file
if (!empty($_FILES['upload']) && $_FILES['upload']['error'] == UPLOAD_ERR_OK) {
    // Be sure we're dealing with an upload
    if (is_uploaded_file($_FILES['upload']['tmp_name']) === false) {
        throw new \Exception('Error on upload: Invalid file definition');
    }

    // Rename the uploaded file
    $uploadName = $_FILES['upload']['name'];
    $ext = strtolower(substr($uploadName, strripos($uploadName, '.')+1));
    $filename = round(microtime(true)).mt_rand().'.'.$ext;

    move_uploaded_file($_FILES['upload']['tmp_name'], __DIR__.'../uploads/'.$filename);
    // Insert it into our tracking along with the original name
}

PHP 安全上传文件 - 服务器配置


通过使用 jhead 之类的工具将自定义代码嵌入映像本身,仍可以绕过上面提到的服务器端验证,并且该文件可能已运行并解释为 PHP。

这就是为什么也应该在服务器级别执行文件类型强制执行的原因。

PHP 安全上传文件 - Apache 配置


确保未将Apache配置为将多个文件解释 为相同文件(例如,将图像解释为PHP文件)。使用ForceType 指令将类型强制为上载的文件。

<FilesMatch "\.(?i:pdf)$">
    ForceType application/octet-stream
    Header set Content-Disposition attachment
</FilesMatch>

或在图片的情况下:

ForceType application/octet-stream
<FilesMatch "(?i).jpe?g$">
    ForceType image/jpeg
</FilesMatch>
<FilesMatch "(?i).gif$">
    ForceType image/gif
</FilesMatch>
<FilesMatch "(?i).png$">
    ForceType image/png
</FilesMatch>

PHP 安全上传文件 - Nginx 配置


在 Nginx 上,您可以使用重写规则,也可以使用 mime.types 默认提供的配置文件。

location ~* (.*\.pdf) {
    types { application/octet-stream .pdf; }
    default_type application/octet-stream;
}
 

PHP 安全上传文件扩展阅读:




发表评论