完整的安全图像上传脚本
Full Secure Image Upload Script
我不知道这是否会发生,但我会尝试的。
在过去的一个小时里,我研究了图片上传的安全性。得知有很多函数可以测试上传。
在我的项目中,我需要保证上传图片的安全。它的数量也可能非常大,并且可能需要大量带宽,因此购买 API 不是一种选择。
所以我决定获取一个完整的 PHP 脚本来真正安全地上传图片。我还认为它会对那里的许多人有所帮助,因为不可能找到真正安全的。但是我不是php方面的专家,所以添加一些功能让我很头疼,所以我会请求这个社区帮助创建一个真正安全的图像上传的完整脚本。
这里有关于这个的非常好的主题(但是,它们只是告诉我们需要什么来完成这个技巧,而不是如何做到这一点,正如我所说,我不是 PHP 方面的大师,所以我不能自己做这一切):
PHP image upload security check list
https://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form
总之,他们告诉我们这是上传安全图像所需要的(我将引用上面的页面):
- Disable PHP from running inside the upload folder using .httaccess.
- Do not allow upload if the file name contains string "php".
- Allow only extensions: jpg,jpeg,gif and png.
- Allow only image file type.
- Disallow image with two file type.
- Change the image name. Upload to a sub-directory not root directory.
Also:
- Re-process the image using GD (or Imagick) and save the processed image. All others are just fun boring for hackers"
- As rr pointed out, use move_uploaded_file() for any upload"
- By the way, you'd want to be very restrictive about your upload folder. Those places are one of the dark corners where many exploits
happen. This is valid for any type of upload and any programming
language/server. Check
https://www.owasp.org/index.php/Unrestricted_File_Upload
- Level 1: Check the extension (extension file ends with)
- Level 2: Check the MIME type ($file_info = getimagesize($_FILES['image_file']; $file_mime = $file_info['mime'];)
- Level 3: Read first 100 bytes and check if they have any bytes in the following range: ASCII 0-8, 12-31 (decimal).
- Level 4: Check for magic numbers in the header (first 10-20 bytes of the file). You can find some of the files header bytes from
here:
http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Examples
- You might want to run "is_uploaded_file" on the $_FILES['my_files']['tmp_name'] as well. See
http://php.net/manual/en/function.is-uploaded-file.php
这是其中的很大一部分,但还不是全部。 (如果您知道更多有助于使上传更安全的信息,请分享。)
这就是我们现在得到的
主要PHP:
function uploadFile ($file_field = null, $check_image = false, $random_name = false) {
//Config Section
//Set file upload path
$path = 'uploads/'; //with trailing slash
//Set max file size in bytes
$max_size = 1000000;
//Set default file extension whitelist
$whitelist_ext = array('jpeg','jpg','png','gif');
//Set default file type whitelist
$whitelist_type = array('image/jpeg', 'image/jpg', 'image/png','image/gif');
//The Validation
// Create an array to hold any output
$out = array('error'=>null);
if (!$file_field) {
$out['error'][] = "Please specify a valid form field name";
}
if (!$path) {
$out['error'][] = "Please specify a valid upload path";
}
if (count($out['error'])>0) {
return $out;
}
//Make sure that there is a file
if((!empty($_FILES[$file_field])) && ($_FILES[$file_field]['error'] == 0)) {
// Get filename
$file_info = pathinfo($_FILES[$file_field]['name']);
$name = $file_info['filename'];
$ext = $file_info['extension'];
//Check file has the right extension
if (!in_array($ext, $whitelist_ext)) {
$out['error'][] = "Invalid file Extension";
}
//Check that the file is of the right type
if (!in_array($_FILES[$file_field]["type"], $whitelist_type)) {
$out['error'][] = "Invalid file Type";
}
//Check that the file is not too big
if ($_FILES[$file_field]["size"] > $max_size) {
$out['error'][] = "File is too big";
}
//If $check image is set as true
if ($check_image) {
if (!getimagesize($_FILES[$file_field]['tmp_name'])) {
$out['error'][] = "Uploaded file is not a valid image";
}
}
//Create full filename including path
if ($random_name) {
// Generate random filename
$tmp = str_replace(array('.',' '), array('',''), microtime());
if (!$tmp || $tmp == '') {
$out['error'][] = "File must have a name";
}
$newname = $tmp.'.'.$ext;
} else {
$newname = $name.'.'.$ext;
}
//Check if file already exists on server
if (file_exists($path.$newname)) {
$out['error'][] = "A file with this name already exists";
}
if (count($out['error'])>0) {
//The file has not correctly validated
return $out;
}
if (move_uploaded_file($_FILES[$file_field]['tmp_name'], $path.$newname)) {
//Success
$out['filepath'] = $path;
$out['filename'] = $newname;
return $out;
} else {
$out['error'][] = "Server Error!";
}
} else {
$out['error'][] = "No file uploaded";
return $out;
}
}
if (isset($_POST['submit'])) {
$file = uploadFile('file', true, true);
if (is_array($file['error'])) {
$message = '';
foreach ($file['error'] as $msg) {
$message .= '<p>'.$msg.'</p>';
}
} else {
$message = "File uploaded successfully".$newname;
}
echo $message;
}
和形式:
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data" name="form1" id="form1">
<input name="file" type="file" id="imagee" />
<input name="submit" type="submit" value="Upload" />
</form>
所以,我要的是通过发布代码片段来帮助我(和其他人)使这个图像上传脚本变得超级安全。
或者通过 sharing/creating 添加了所有片段的完整脚本。
在 PHP 中上传文件既简单又安全。
我建议了解:
- pathinfo - Returns 文件路径信息
- move_uploaded_file - 将上传的文件移动到新位置
- copy - 复制一个文件
- finfo_open - 创建新的
fileinfo
资源
要在 PHP 中上传文件,您有两种方法:PUT
和 POST
。
要将 POST
方法与 HTML 一起使用,您需要像这样在表单上启用 enctype
:
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
然后在 PHP 中,您需要使用 $_FILES
获取上传的文件,如下所示:
$_FILES['file']
然后您需要使用 move_uploaded_file
:
从 temp("upload") 移动文件
if (move_uploaded_file($_FILES['file']['tmp_name'], YOUR_PATH)) {
// ...
}
上传文件后,您需要检查文件的扩展名。最好的方法是像这样使用 pathinfo
:
$extension = pathinfo($_FILES['file']['tmp_name'], PATHINFO_EXTENSION);
但扩展名不安全,因为您可以上传扩展名为 .jpg
但 mimetype text/php
的文件,这是一个后门。
所以,我建议使用 finfo_open
检查真实的 mimetype,如下所示:
$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['file']['tmp_name']);
并且不要使用 $_FILES['file']['type']
因为有时候,
根据您的浏览器和客户端 OS,您可能会收到
application/octet-stream
而这个 mimetype 不是真实的
您上传文件的 mimetype。
我认为您可以使用此方案安全地上传文件。
对不起我的英语,再见!
当您开始使用安全的图片上传脚本时,需要考虑很多事情。现在我离这方面的专家还差得很远,但过去我曾被要求开发过一次。我将介绍我在这里经历的整个过程,以便您可以跟进。为此,我将从一个非常基本的 html 表单和处理文件的 php 脚本开始。
HTML形式:
<form name="upload" action="upload.php" method="POST" enctype="multipart/form-data">
Select image to upload: <input type="file" name="image">
<input type="submit" name="upload" value="upload">
</form>
PHP 文件:
<?php
$uploaddir = 'uploads/';
$uploadfile = $uploaddir . basename($_FILES['image']['name']);
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>
第一个问题:文件类型
攻击者不必使用您网站上的表单将文件上传到您的服务器。 POST 可以通过多种方式拦截请求。想想浏览器插件、代理、Perl 脚本。无论我们多么努力,我们都无法阻止攻击者尝试上传他们不应该上传的内容。所以我们所有的安全都必须在服务器端完成。
第一个问题是文件类型。在上面的脚本中,攻击者可以上传他们想要的任何东西,例如 php 脚本,然后直接按照 link 来执行它。所以为了防止这种情况,我们实现 Content-type 验证:
<?php
if($_FILES['image']['type'] != "image/png") {
echo "Only PNG images are allowed!";
exit;
}
$uploaddir = 'uploads/';
$uploadfile = $uploaddir . basename($_FILES['image']['name']);
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>
不幸的是,这还不够。正如我之前提到的,攻击者可以完全控制请求。没有什么可以阻止 him/her 修改请求 header 并且只需将内容类型更改为“image/png”。因此,与其仅仅依赖 Content-type header,还不如验证上传文件的内容。这就是 php GD 库派上用场的地方。使用 getimagesize()
,我们将使用 GD 库处理图像。如果不是图片,这将失败,因此整个上传将失败:
<?php
$verifyimg = getimagesize($_FILES['image']['tmp_name']);
if($verifyimg['mime'] != 'image/png') {
echo "Only PNG images are allowed!";
exit;
}
$uploaddir = 'uploads/';
$uploadfile = $uploaddir . basename($_FILES['image']['name']);
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>
不过我们还没有到那一步。大多数图像文件类型都允许向其添加文本注释。同样,没有什么能阻止攻击者添加一些 php 代码作为注释。 GD 库会将其评估为完全有效的图像。 PHP 解释器将完全忽略注释中的图像和 运行 php 代码。的确,它取决于 php 配置,哪些文件扩展名由 php 解释器处理,哪些不处理,但是由于有许多开发人员无法控制此配置,因为使用VPS,我们不能假设 php 解释器不会处理图像。这就是为什么添加文件扩展名白名单也不够安全。
此问题的解决方案是将图像存储在攻击者无法直接访问文件的位置。这可能在文档根目录之外或在受 .htaccess 文件保护的目录中:
order deny,allow
deny from all
allow from 127.0.0.1
编辑:在与其他一些 PHP 程序员交谈后,我强烈建议使用文档根目录之外的文件夹,因为 htaccess 并不总是可靠的。
我们仍然需要用户或任何其他访问者能够查看图像。所以我们将使用 php 为他们检索图像:
<?php
$uploaddir = 'uploads/';
$name = $_GET['name']; // Assuming the file name is in the URL for this example
readfile($uploaddir.$name);
?>
问题二:本地文件包含攻击
虽然我们的脚本现在相当安全,但我们不能假设服务器不会受到其他漏洞的影响。一个常见的安全漏洞称为 本地文件包含 。为了解释这一点,我需要添加一个示例代码:
<?php
if(isset($_COOKIE['lang'])) {
$lang = $_COOKIE['lang'];
} elseif (isset($_GET['lang'])) {
$lang = $_GET['lang'];
} else {
$lang = 'english';
}
include("language/$lang.php");
?>
在此示例中,我们讨论的是多语言网站。站点语言不被视为“高风险”信息。我们尝试通过 cookie 或 GET 请求获取访问者的首选语言,并根据它包含所需的文件。现在考虑当攻击者输入以下 url:
时会发生什么
www.example.com/index.php?lang=../uploads/my_evil_image.jpg
PHP 将包含攻击者上传的文件,绕过他们无法直接访问该文件的事实,我们回到了第一步。
这个问题的解决方法是确保用户不知道服务器上的文件名。相反,我们将使用数据库更改文件名甚至扩展名来跟踪它:
CREATE TABLE `uploads` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL,
`original_name` VARCHAR(64) NOT NULL,
`mime_type` VARCHAR(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
<?php
if(!empty($_POST['upload']) && !empty($_FILES['image']) && $_FILES['image']['error'] == 0)) {
$uploaddir = 'uploads/';
/* Generates random filename and extension */
function tempnam_sfx($path, $suffix){
do {
$file = $path."/".mt_rand().$suffix;
$fp = @fopen($file, 'x');
}
while(!$fp);
fclose($fp);
return $file;
}
/* Process image with GD library */
$verifyimg = getimagesize($_FILES['image']['tmp_name']);
/* Make sure the MIME type is an image */
$pattern = "#^(image/)[^\s\n<]+$#i";
if(!preg_match($pattern, $verifyimg['mime']){
die("Only image files are allowed!");
}
/* Rename both the image and the extension */
$uploadfile = tempnam_sfx($uploaddir, ".tmp");
/* Upload the file to a secure directory with the new name and extension */
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";
// Set DSN
$dsn = 'mysql:host='.$dbhost.';dbname='.$dbname;
// Set options
$options = array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
try {
$db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}
/* Setup query */
$query = 'INSERT INTO uploads (name, original_name, mime_type) VALUES (:name, :oriname, :mime)';
/* Prepare query */
$db->prepare($query);
/* Bind parameters */
$db->bindParam(':name', basename($uploadfile));
$db->bindParam(':oriname', basename($_FILES['image']['name']));
$db->bindParam(':mime', $_FILES['image']['type']);
/* Execute query */
try {
$db->execute();
}
catch(PDOException $e){
// Remove the uploaded file
unlink($uploadfile);
die("Error!: " . $e->getMessage());
}
} else {
die("Image upload failed!");
}
}
?>
现在我们完成了以下操作:
- 我们创建了一个安全的地方来保存图像
- 我们已经用 GD 库处理了图像
- 我们检查了图片的 MIME 类型
- 我们重命名了文件名并更改了扩展名
- 我们已将新文件名和原始文件名保存在我们的数据库中
- 我们还在数据库中保存了 MIME 类型
我们仍然需要能够向访问者显示图像。我们只需使用我们数据库的 id 列来执行此操作:
<?php
$uploaddir = 'uploads/';
$id = 1;
/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";
// Set DSN
$dsn = 'mysql:host='.$dbhost.';dbname='.$dbname;
// Set options
$options = array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
try {
$db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}
/* Setup query */
$query = 'SELECT name, original_name, mime_type FROM uploads WHERE id=:id';
/* Prepare query */
$db->prepare($query);
/* Bind parameters */
$db->bindParam(':id', $id);
/* Execute query */
try {
$db->execute();
$result = $db->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}
/* Get the original filename */
$newfile = $result['original_name'];
/* Send headers and file to visitor */
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename='.basename($newfile));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($uploaddir.$result['name']));
header("Content-Type: " . $result['mime_type']);
readfile($uploaddir.$result['name']);
?>
感谢这个脚本,访问者将能够查看图像或使用原始文件名下载图像。但是,他们无法直接访问您服务器上的文件,也无法欺骗您的服务器以访问 him/her 的文件,因为他们无法知道它是哪个文件。他们也不能暴力破解你的上传目录,因为它根本不允许任何人e 访问服务器本身以外的目录。
我的安全图片上传脚本到此结束。
我想补充一点,我没有在此脚本中包含最大文件大小,但您自己应该可以轻松做到这一点。
图片上传Class
由于此脚本的高需求,我编写了一个 ImageUpload class,它应该可以让你们所有人更轻松地安全地处理网站访问者上传的图像。 class 可以同时处理单个文件和多个文件,并为您提供显示、下载和删除图像等附加功能。
由于这里的代码太大了 post,你可以在这里从 MEGA 下载 class:
只需阅读 README.txt 并按照说明操作即可。
走向开源
Image Secure class 项目现在也可以在我的 Github 个人资料中使用。这样一来,其他人(您?)就可以为该项目做出贡献,并使它成为一个适合所有人的优秀图书馆。
这是另一个提示。不要依赖 ['type'] 元素,它太不可靠了。而是检查文件头本身以查看文件类型实际是什么。像这样:
<?php
// open the file and check header
$tempfile = $FILES['tmp_name'];
if (!($handle = fopen($tempfile, 'rb')))
{
echo 'open file failed';
fclose($handle);
exit;
}else{
$hdr = fread($handle, 12); //should grab first 12 of header
fclose($handle);
//now check the header results
$subheaderpre = substr($hdr, 0, 12);
$subheader = trim($subheaderpre);
//get hex value to check png
$getbytes = substr($subheader, 0, 8);
$hxval = bin2hex($getbytes);
if ((substr($subheader, 0, 4) == "\xff\xd8\xff\xe0") && (substr($subheader, 6, 5) == "JFIF\x00"))
{
//passed jpg test
}elseif($hxval == "89504e470d0a1a" || substr($subheader, 0, 8) == "\x89PNG\x0d\x0a\x1a\x0a")
{
//passed png test
}else{
//fail both
echo 'Sorry but image failed to validate, try another image';
exit;
}//close else elseif else
}//close else ! $handle
以下代码对我来说工作正常谢谢
function gen_uid($l=5){
return substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"), 10, $l);
}
$mime_type = mime_content_type($_FILES['imglink']['tmp_name']);
$allowed_file_types = ['image/png', 'image/jpeg'];
if (! in_array($mime_type, $allowed_file_types)) {
// File type is NOT allowed.
echo "Only PNG and JPG images are allowed!";
exit;
}
$uploaddir = '../uploads/images/';
$name = pathinfo($_FILES['imglink']['name'], PATHINFO_FILENAME);
$ext = pathinfo($_FILES['imglink']['name'], PATHINFO_EXTENSION);
$newname = gen_uid(rand(0,30));
$uploadfile = $uploaddir.$newname.".webp";
if (move_uploaded_file($_FILES['imglink']['tmp_name'], $uploadfile)) {
$imglink = basename($uploadfile);
} else {
echo "Image upload failed!";
die();
}
我不知道这是否会发生,但我会尝试的。
在过去的一个小时里,我研究了图片上传的安全性。得知有很多函数可以测试上传。
在我的项目中,我需要保证上传图片的安全。它的数量也可能非常大,并且可能需要大量带宽,因此购买 API 不是一种选择。
所以我决定获取一个完整的 PHP 脚本来真正安全地上传图片。我还认为它会对那里的许多人有所帮助,因为不可能找到真正安全的。但是我不是php方面的专家,所以添加一些功能让我很头疼,所以我会请求这个社区帮助创建一个真正安全的图像上传的完整脚本。
这里有关于这个的非常好的主题(但是,它们只是告诉我们需要什么来完成这个技巧,而不是如何做到这一点,正如我所说,我不是 PHP 方面的大师,所以我不能自己做这一切): PHP image upload security check list https://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form
总之,他们告诉我们这是上传安全图像所需要的(我将引用上面的页面):
- Disable PHP from running inside the upload folder using .httaccess.
- Do not allow upload if the file name contains string "php".
- Allow only extensions: jpg,jpeg,gif and png.
- Allow only image file type.
- Disallow image with two file type.
- Change the image name. Upload to a sub-directory not root directory.
Also:
- Re-process the image using GD (or Imagick) and save the processed image. All others are just fun boring for hackers"
- As rr pointed out, use move_uploaded_file() for any upload"
- By the way, you'd want to be very restrictive about your upload folder. Those places are one of the dark corners where many exploits
happen. This is valid for any type of upload and any programming
language/server. Check
https://www.owasp.org/index.php/Unrestricted_File_Upload- Level 1: Check the extension (extension file ends with)
- Level 2: Check the MIME type ($file_info = getimagesize($_FILES['image_file']; $file_mime = $file_info['mime'];)
- Level 3: Read first 100 bytes and check if they have any bytes in the following range: ASCII 0-8, 12-31 (decimal).
- Level 4: Check for magic numbers in the header (first 10-20 bytes of the file). You can find some of the files header bytes from here:
http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Examples- You might want to run "is_uploaded_file" on the $_FILES['my_files']['tmp_name'] as well. See
http://php.net/manual/en/function.is-uploaded-file.php
这是其中的很大一部分,但还不是全部。 (如果您知道更多有助于使上传更安全的信息,请分享。)
这就是我们现在得到的
主要PHP:
function uploadFile ($file_field = null, $check_image = false, $random_name = false) { //Config Section //Set file upload path $path = 'uploads/'; //with trailing slash //Set max file size in bytes $max_size = 1000000; //Set default file extension whitelist $whitelist_ext = array('jpeg','jpg','png','gif'); //Set default file type whitelist $whitelist_type = array('image/jpeg', 'image/jpg', 'image/png','image/gif'); //The Validation // Create an array to hold any output $out = array('error'=>null); if (!$file_field) { $out['error'][] = "Please specify a valid form field name"; } if (!$path) { $out['error'][] = "Please specify a valid upload path"; } if (count($out['error'])>0) { return $out; } //Make sure that there is a file if((!empty($_FILES[$file_field])) && ($_FILES[$file_field]['error'] == 0)) { // Get filename $file_info = pathinfo($_FILES[$file_field]['name']); $name = $file_info['filename']; $ext = $file_info['extension']; //Check file has the right extension if (!in_array($ext, $whitelist_ext)) { $out['error'][] = "Invalid file Extension"; } //Check that the file is of the right type if (!in_array($_FILES[$file_field]["type"], $whitelist_type)) { $out['error'][] = "Invalid file Type"; } //Check that the file is not too big if ($_FILES[$file_field]["size"] > $max_size) { $out['error'][] = "File is too big"; } //If $check image is set as true if ($check_image) { if (!getimagesize($_FILES[$file_field]['tmp_name'])) { $out['error'][] = "Uploaded file is not a valid image"; } } //Create full filename including path if ($random_name) { // Generate random filename $tmp = str_replace(array('.',' '), array('',''), microtime()); if (!$tmp || $tmp == '') { $out['error'][] = "File must have a name"; } $newname = $tmp.'.'.$ext; } else { $newname = $name.'.'.$ext; } //Check if file already exists on server if (file_exists($path.$newname)) { $out['error'][] = "A file with this name already exists"; } if (count($out['error'])>0) { //The file has not correctly validated return $out; } if (move_uploaded_file($_FILES[$file_field]['tmp_name'], $path.$newname)) { //Success $out['filepath'] = $path; $out['filename'] = $newname; return $out; } else { $out['error'][] = "Server Error!"; } } else { $out['error'][] = "No file uploaded"; return $out; } } if (isset($_POST['submit'])) { $file = uploadFile('file', true, true); if (is_array($file['error'])) { $message = ''; foreach ($file['error'] as $msg) { $message .= '<p>'.$msg.'</p>'; } } else { $message = "File uploaded successfully".$newname; } echo $message; }
和形式:
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data" name="form1" id="form1"> <input name="file" type="file" id="imagee" /> <input name="submit" type="submit" value="Upload" /> </form>
所以,我要的是通过发布代码片段来帮助我(和其他人)使这个图像上传脚本变得超级安全。 或者通过 sharing/creating 添加了所有片段的完整脚本。
在 PHP 中上传文件既简单又安全。 我建议了解:
- pathinfo - Returns 文件路径信息
- move_uploaded_file - 将上传的文件移动到新位置
- copy - 复制一个文件
- finfo_open - 创建新的
fileinfo
资源
要在 PHP 中上传文件,您有两种方法:PUT
和 POST
。
要将 POST
方法与 HTML 一起使用,您需要像这样在表单上启用 enctype
:
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
然后在 PHP 中,您需要使用 $_FILES
获取上传的文件,如下所示:
$_FILES['file']
然后您需要使用 move_uploaded_file
:
if (move_uploaded_file($_FILES['file']['tmp_name'], YOUR_PATH)) {
// ...
}
上传文件后,您需要检查文件的扩展名。最好的方法是像这样使用 pathinfo
:
$extension = pathinfo($_FILES['file']['tmp_name'], PATHINFO_EXTENSION);
但扩展名不安全,因为您可以上传扩展名为 .jpg
但 mimetype text/php
的文件,这是一个后门。
所以,我建议使用 finfo_open
检查真实的 mimetype,如下所示:
$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['file']['tmp_name']);
并且不要使用 $_FILES['file']['type']
因为有时候,
根据您的浏览器和客户端 OS,您可能会收到
application/octet-stream
而这个 mimetype 不是真实的
您上传文件的 mimetype。
我认为您可以使用此方案安全地上传文件。
对不起我的英语,再见!
当您开始使用安全的图片上传脚本时,需要考虑很多事情。现在我离这方面的专家还差得很远,但过去我曾被要求开发过一次。我将介绍我在这里经历的整个过程,以便您可以跟进。为此,我将从一个非常基本的 html 表单和处理文件的 php 脚本开始。
HTML形式:
<form name="upload" action="upload.php" method="POST" enctype="multipart/form-data">
Select image to upload: <input type="file" name="image">
<input type="submit" name="upload" value="upload">
</form>
PHP 文件:
<?php
$uploaddir = 'uploads/';
$uploadfile = $uploaddir . basename($_FILES['image']['name']);
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>
第一个问题:文件类型
攻击者不必使用您网站上的表单将文件上传到您的服务器。 POST 可以通过多种方式拦截请求。想想浏览器插件、代理、Perl 脚本。无论我们多么努力,我们都无法阻止攻击者尝试上传他们不应该上传的内容。所以我们所有的安全都必须在服务器端完成。
第一个问题是文件类型。在上面的脚本中,攻击者可以上传他们想要的任何东西,例如 php 脚本,然后直接按照 link 来执行它。所以为了防止这种情况,我们实现 Content-type 验证:
<?php
if($_FILES['image']['type'] != "image/png") {
echo "Only PNG images are allowed!";
exit;
}
$uploaddir = 'uploads/';
$uploadfile = $uploaddir . basename($_FILES['image']['name']);
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>
不幸的是,这还不够。正如我之前提到的,攻击者可以完全控制请求。没有什么可以阻止 him/her 修改请求 header 并且只需将内容类型更改为“image/png”。因此,与其仅仅依赖 Content-type header,还不如验证上传文件的内容。这就是 php GD 库派上用场的地方。使用 getimagesize()
,我们将使用 GD 库处理图像。如果不是图片,这将失败,因此整个上传将失败:
<?php
$verifyimg = getimagesize($_FILES['image']['tmp_name']);
if($verifyimg['mime'] != 'image/png') {
echo "Only PNG images are allowed!";
exit;
}
$uploaddir = 'uploads/';
$uploadfile = $uploaddir . basename($_FILES['image']['name']);
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
echo "Image succesfully uploaded.";
} else {
echo "Image uploading failed.";
}
?>
不过我们还没有到那一步。大多数图像文件类型都允许向其添加文本注释。同样,没有什么能阻止攻击者添加一些 php 代码作为注释。 GD 库会将其评估为完全有效的图像。 PHP 解释器将完全忽略注释中的图像和 运行 php 代码。的确,它取决于 php 配置,哪些文件扩展名由 php 解释器处理,哪些不处理,但是由于有许多开发人员无法控制此配置,因为使用VPS,我们不能假设 php 解释器不会处理图像。这就是为什么添加文件扩展名白名单也不够安全。
此问题的解决方案是将图像存储在攻击者无法直接访问文件的位置。这可能在文档根目录之外或在受 .htaccess 文件保护的目录中:
order deny,allow
deny from all
allow from 127.0.0.1
编辑:在与其他一些 PHP 程序员交谈后,我强烈建议使用文档根目录之外的文件夹,因为 htaccess 并不总是可靠的。
我们仍然需要用户或任何其他访问者能够查看图像。所以我们将使用 php 为他们检索图像:
<?php
$uploaddir = 'uploads/';
$name = $_GET['name']; // Assuming the file name is in the URL for this example
readfile($uploaddir.$name);
?>
问题二:本地文件包含攻击
虽然我们的脚本现在相当安全,但我们不能假设服务器不会受到其他漏洞的影响。一个常见的安全漏洞称为 本地文件包含 。为了解释这一点,我需要添加一个示例代码:
<?php
if(isset($_COOKIE['lang'])) {
$lang = $_COOKIE['lang'];
} elseif (isset($_GET['lang'])) {
$lang = $_GET['lang'];
} else {
$lang = 'english';
}
include("language/$lang.php");
?>
在此示例中,我们讨论的是多语言网站。站点语言不被视为“高风险”信息。我们尝试通过 cookie 或 GET 请求获取访问者的首选语言,并根据它包含所需的文件。现在考虑当攻击者输入以下 url:
时会发生什么www.example.com/index.php?lang=../uploads/my_evil_image.jpg
PHP 将包含攻击者上传的文件,绕过他们无法直接访问该文件的事实,我们回到了第一步。
这个问题的解决方法是确保用户不知道服务器上的文件名。相反,我们将使用数据库更改文件名甚至扩展名来跟踪它:
CREATE TABLE `uploads` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL,
`original_name` VARCHAR(64) NOT NULL,
`mime_type` VARCHAR(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
<?php
if(!empty($_POST['upload']) && !empty($_FILES['image']) && $_FILES['image']['error'] == 0)) {
$uploaddir = 'uploads/';
/* Generates random filename and extension */
function tempnam_sfx($path, $suffix){
do {
$file = $path."/".mt_rand().$suffix;
$fp = @fopen($file, 'x');
}
while(!$fp);
fclose($fp);
return $file;
}
/* Process image with GD library */
$verifyimg = getimagesize($_FILES['image']['tmp_name']);
/* Make sure the MIME type is an image */
$pattern = "#^(image/)[^\s\n<]+$#i";
if(!preg_match($pattern, $verifyimg['mime']){
die("Only image files are allowed!");
}
/* Rename both the image and the extension */
$uploadfile = tempnam_sfx($uploaddir, ".tmp");
/* Upload the file to a secure directory with the new name and extension */
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";
// Set DSN
$dsn = 'mysql:host='.$dbhost.';dbname='.$dbname;
// Set options
$options = array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
try {
$db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}
/* Setup query */
$query = 'INSERT INTO uploads (name, original_name, mime_type) VALUES (:name, :oriname, :mime)';
/* Prepare query */
$db->prepare($query);
/* Bind parameters */
$db->bindParam(':name', basename($uploadfile));
$db->bindParam(':oriname', basename($_FILES['image']['name']));
$db->bindParam(':mime', $_FILES['image']['type']);
/* Execute query */
try {
$db->execute();
}
catch(PDOException $e){
// Remove the uploaded file
unlink($uploadfile);
die("Error!: " . $e->getMessage());
}
} else {
die("Image upload failed!");
}
}
?>
现在我们完成了以下操作:
- 我们创建了一个安全的地方来保存图像
- 我们已经用 GD 库处理了图像
- 我们检查了图片的 MIME 类型
- 我们重命名了文件名并更改了扩展名
- 我们已将新文件名和原始文件名保存在我们的数据库中
- 我们还在数据库中保存了 MIME 类型
我们仍然需要能够向访问者显示图像。我们只需使用我们数据库的 id 列来执行此操作:
<?php
$uploaddir = 'uploads/';
$id = 1;
/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";
// Set DSN
$dsn = 'mysql:host='.$dbhost.';dbname='.$dbname;
// Set options
$options = array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
try {
$db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}
/* Setup query */
$query = 'SELECT name, original_name, mime_type FROM uploads WHERE id=:id';
/* Prepare query */
$db->prepare($query);
/* Bind parameters */
$db->bindParam(':id', $id);
/* Execute query */
try {
$db->execute();
$result = $db->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e){
die("Error!: " . $e->getMessage());
}
/* Get the original filename */
$newfile = $result['original_name'];
/* Send headers and file to visitor */
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename='.basename($newfile));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($uploaddir.$result['name']));
header("Content-Type: " . $result['mime_type']);
readfile($uploaddir.$result['name']);
?>
感谢这个脚本,访问者将能够查看图像或使用原始文件名下载图像。但是,他们无法直接访问您服务器上的文件,也无法欺骗您的服务器以访问 him/her 的文件,因为他们无法知道它是哪个文件。他们也不能暴力破解你的上传目录,因为它根本不允许任何人e 访问服务器本身以外的目录。
我的安全图片上传脚本到此结束。
我想补充一点,我没有在此脚本中包含最大文件大小,但您自己应该可以轻松做到这一点。
图片上传Class
由于此脚本的高需求,我编写了一个 ImageUpload class,它应该可以让你们所有人更轻松地安全地处理网站访问者上传的图像。 class 可以同时处理单个文件和多个文件,并为您提供显示、下载和删除图像等附加功能。
由于这里的代码太大了 post,你可以在这里从 MEGA 下载 class:
只需阅读 README.txt 并按照说明操作即可。
走向开源
Image Secure class 项目现在也可以在我的 Github 个人资料中使用。这样一来,其他人(您?)就可以为该项目做出贡献,并使它成为一个适合所有人的优秀图书馆。
这是另一个提示。不要依赖 ['type'] 元素,它太不可靠了。而是检查文件头本身以查看文件类型实际是什么。像这样:
<?php
// open the file and check header
$tempfile = $FILES['tmp_name'];
if (!($handle = fopen($tempfile, 'rb')))
{
echo 'open file failed';
fclose($handle);
exit;
}else{
$hdr = fread($handle, 12); //should grab first 12 of header
fclose($handle);
//now check the header results
$subheaderpre = substr($hdr, 0, 12);
$subheader = trim($subheaderpre);
//get hex value to check png
$getbytes = substr($subheader, 0, 8);
$hxval = bin2hex($getbytes);
if ((substr($subheader, 0, 4) == "\xff\xd8\xff\xe0") && (substr($subheader, 6, 5) == "JFIF\x00"))
{
//passed jpg test
}elseif($hxval == "89504e470d0a1a" || substr($subheader, 0, 8) == "\x89PNG\x0d\x0a\x1a\x0a")
{
//passed png test
}else{
//fail both
echo 'Sorry but image failed to validate, try another image';
exit;
}//close else elseif else
}//close else ! $handle
以下代码对我来说工作正常谢谢
function gen_uid($l=5){
return substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"), 10, $l);
}
$mime_type = mime_content_type($_FILES['imglink']['tmp_name']);
$allowed_file_types = ['image/png', 'image/jpeg'];
if (! in_array($mime_type, $allowed_file_types)) {
// File type is NOT allowed.
echo "Only PNG and JPG images are allowed!";
exit;
}
$uploaddir = '../uploads/images/';
$name = pathinfo($_FILES['imglink']['name'], PATHINFO_FILENAME);
$ext = pathinfo($_FILES['imglink']['name'], PATHINFO_EXTENSION);
$newname = gen_uid(rand(0,30));
$uploadfile = $uploaddir.$newname.".webp";
if (move_uploaded_file($_FILES['imglink']['tmp_name'], $uploadfile)) {
$imglink = basename($uploadfile);
} else {
echo "Image upload failed!";
die();
}