完全安全的图像上传脚本
我不知道这是否会发生,但我会尝试。
在过去的一个小时里,我做了图像上传安全的研究 我了解到有很多function来testing上传。
在我的项目中,我需要安全地上传图片。 也可能有一个非常大的数量,它可能需要大量的带宽,所以购买API不是一个选项。
所以我决定获得一个完整的PHP脚本来实现真正安全的图片上传。 我也认为这对很多人来说都是有帮助的,因为find真正安全的人是不可能的。 但是我不是php的专家,所以添加一些函数让我很头疼,所以我会要求这个社区帮助创build一个真正安全的图片上传的完整脚本。
真正伟大的话题在这里(但是,他们只是告诉什么是需要做的伎俩,而不是如何做到这一点,正如我所说我不是一个PHP的高手,所以我不能够这样做由我自己): PHP图像上传安全检查列表 https://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form
总之,他们告诉说,这是安全图像上传所需要的(我将从上面的页面引用):
- 使用.httaccess禁用PHP在上传文件夹内运行。
- 如果文件名包含string“php”,则不允许上传。
- 只允许扩展名:jpg,jpeg,gif和png。
- 只允许图像文件types。
- 不允许使用两种文件types的图像。
- 更改图像名称。 上传到不是根目录的子目录。
也:
- 使用GD(或Imagick)重新处理图像并保存处理后的图像。 所有其他人只是对黑客乐趣无趣“
- 正如rr指出的,使用move_uploaded_file()来进行上传“
- 顺便说一下,你想要限制你的上传文件夹。 那些地方是许多剥削的黑暗angular落之一
发生。 这适用于任何types的上传和任何编程
语言/服务器。 检查
https://www.owasp.org/index.php/Unrestricted_File_Upload- 等级1:检查分机(分机文件结尾)
- 级别2:检查MIMEtypes($ file_info = getimagesize($ _ FILES ['image_file']; $ file_mime = $ file_info ['mime'];)
- 等级3:读取前100个字节,并检查它们是否在以下范围内有任何字节:ASCII 0-8,12-31(十进制)。
- 等级4:检查标题中的幻数(文件的前10-20个字节)。 你可以从这里find一些文件头字节:
http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Examples- 您也可以在$ _FILES ['my_files'] ['tmp_name']上运行“is_uploaded_file”。 看到
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; }
-
forms如下:
<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>
所以,我所要求的是通过发布代码片段来帮助我(和其他人)使这个图像上传脚本超级安全。 或者通过共享/创build一个完整的脚本添加所有的片段。
当你开始一个安全的图像上传脚本的工作,有很多事情需要考虑。 现在我不是靠近这方面的专家,但我曾经被要求过去发展一次。 我要走过我在这里经历的整个过程,所以你可以跟随。 为此,我将从处理文件的非常基本的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."; } ?>
第一个问题:文件types
攻击者不必使用您的网站上的表格上传文件到您的服务器。 POST请求可以通过多种方式截获。 考虑浏览器插件,代理,Perl脚本。 不pipe我们多努力,我们都无法阻止攻击者尝试上传他不应该的东西。 所以我们所有的安全性都必须在服务器端完成。
第一个问题是文件types。 在上面的脚本中,攻击者可以上传他想要的任何东西,比如php脚本,然后直接执行。 为了防止这种情况,我们实施了内容typesvalidation :
<?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."; } ?>
不幸的是这还不够。 正如我之前提到的,攻击者完全可以控制请求。 没有什么会阻止他/她修改请求标题,只是将内容types改为“image / png”。 因此,不要只依赖Content-type头,最好还要validation上传文件的内容。 这里是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."; } ?>
尽pipe如此,我们还没有到来。 大多数图像文件types允许将文本注释添加到它们。 同样,没有什么能够阻止攻击者添加一些php代码作为评论。 GD图书馆将评估这是一个完全有效的图像。 PHP解释器会完全忽略图像,并在注释中运行php代码。 这是真的,这取决于PHPconfiguration哪些文件扩展名由php解释器处理,哪些不是,但由于有许多开发人员由于使用VPS而无法控制此configuration,所以我们不能假定PHP解释器不会处理图像。 这就是为什么添加一个文件扩展名白名单是不够安全的。
解决方法是将图像存储在攻击者无法直接访问文件的位置。 这可能位于文档根目录之外,或位于受.htaccess文件保护的目录中:
order deny,allow deny from all allow from 127.0.0.1
编辑:在与其他一些PHP程序员谈话后,我强烈build议在文档根目录之外使用一个文件夹,因为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时会发生什么:
http://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!"); } } ?>
所以现在我们做了以下工作:
- 我们已经创build了一个安全的地方来保存图像
- 我们已经用GD库处理了图像
- 我们已经检查了图片的MIMEtypes
- 我们已经重命名了文件名并更改了扩展名
- 我们在数据库中保存了新的和原始的文件名
- 我们也在我们的数据库中保存了MIMEtypes
我们仍然需要能够向访客展示图片。 我们只需使用我们的数据库的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']); ?>
由于这个脚本,访问者将能够查看图像或下载它的原始文件名。 但是,他不能直接访问服务器上的文件,也不能欺骗你的服务器来访问他/她的文件,因为他无法知道它是哪个文件。 (S)他不能强迫你的上传目录,因为它只是不允许任何人访问目录,除了服务器本身。
这就结束了我的安全图像上传脚本。
我想补充一点,我没有在这个脚本中包含一个最大的文件大小,但是你应该很容易就能够自己做到这一点。
ImageUpload类
由于这个脚本的高要求,我写了一个ImageUpload类,它可以让你们更容易地安全地处理你的网站访问者上传的图片。 该类可以同时处理单个文件和多个文件,并为您提供显示,下载和删除图像等附加function。
由于代码只是大到可以在这里发布,所以你可以在这里下载MEGA的类:
下载ImageUpload类
只要阅读README.txt并按照说明操作。
那么,用PHP上传文件太简单而且安全。 我build议学习:
- pathinfo – 返回关于文件path的信息
- move_uploaded_file – 将上传的文件移动到新位置
- 复制 – 复制文件
- finfo_open – 创build一个新的文件信息资源
要用PHP上传文件,你有两个方法,PUT和POST(也许更多..)。 要使用HTML的POST方法,需要在FORM上启用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'], YOU_PATH)) { // ... }
上传文件后,您需要检查扩展名,最好和最好的方法是使用这样的pathinfo
:
$extension = pathinfo($_FILES['file']['tmp_name'],PATHINFO_EXTENSION);
但是扩展名不是安全的,因为你可以用扩展名.jpg上传文件,但是用mimetype text/php
,这是一个后门。 所以,我build议检查真正的MIMEtypes与finfo_open
像这样:
$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['file']['tmp_name']);
而且,不要使用
$_FILES['file']['type']
因为有时并且依赖于浏览器和客户端操作系统,您可能会收到application/octet-stream
,而这个mimetype no是您上传文件的真正模拟types。
我想用这个你可以上传安全文件。
对不起,我的英文,再见!