move_uploaded_file()
函数实现的。为了安全起见,必须验证上传的文件。当上传没有得到适当的保护时,可能会发生很多黑客攻击。始终确保执行服务器端验证,以便能够安全地上载,并确保您了解造成这种情况的原因以及否则可能会遇到的安全漏洞。
为避免目录遍历(也称为路径遍历)攻击,请使用 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"); } }
重命名上载的文件可以避免在上载目标中出现重复的名称,还有助于防止目录遍历攻击。如果需要保留原始文件名,则可以将其保存在数据库中以备将来使用。例如,使用 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;
不用依赖文件扩展名,可以使用 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.'); }
检查上载的文件大小对于不使服务器过载文件很重要。检查上传的文件大小时,有几个主要级别需要研究。
upload_max_filesize
INI 指令
最重要的是限制 php.ini
文件中的 upload_max_filesizeand
、post_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 端有效。
它通过以下方式限制了上传过程:
upload_max_filesize
ini指令。max_file_size
字段是否已定义以及当前上传的字节是否大于该字段。如果是,它将中断上传过程,并在中设置 UPLOAD_ERR_FORM_SIZE
错误代码 $_FILES['key']['error']
。这样,用户无需等待文件的 100% 上载,而是只上载了 max_file_size
字段值字节,并且应用程序停止了上载过程。最好将上传的文件 https://commandnotfound.cn/uploads 保存到公共不可访问的文件夹中,而不是将上传的文件保存到位于的公共位置 。为了传递这些文件,使用了所谓的代理脚本。
为了获得更好的用户体验,HTML 提供了accept 属性,以通过 HTML 中 的扩展名或 mime-type 限制文件类型,因此用户可以即时查看验证错误,并在浏览器中仅选择允许的文件类型。但是, 在撰写本文时,浏览器支持受到限制。请记住,黑客可以轻松绕过客户端验证。上面说明的服务器端验证步骤是要使用的更重要的验证形式。
让我们考虑以上所有内容,并看一个非常简单的示例:
<?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 }
通过使用 jhead 之类的工具将自定义代码嵌入映像本身,仍可以绕过上面提到的服务器端验证,并且该文件可能已运行并解释为 PHP。
这就是为什么也应该在服务器级别执行文件类型强制执行的原因。
确保未将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>
在 Nginx 上,您可以使用重写规则,也可以使用 mime.types
默认提供的配置文件。
location ~* (.*\.pdf) { types { application/octet-stream .pdf; } default_type application/octet-stream; }