当您开始使用安全的图像上传脚本时,需要考虑很多事情。现在,我在这方面尚无专家,但过去曾有人要求我进行开发。我将完成我在这里经历的整个过程,以便您可以继续。为此,我将从一个非常基本的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脚本),然后点击直接链接来执行它。因此,为防止这种情况,我们实施了
Content-type验证 :
<?phpif($_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.";}?>不幸的是,这还不够。如前所述,攻击者可以完全控制该请求。没有什么可以阻止他/她修改请求标头,只需将Content类型更改为“ image /
png”。因此,不仅要依赖Content-type标头,还应该验证上传文件的内容。这是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,allowdeny from allallow from 127.0.0.1
编辑:与其他PHP程序员交谈后,我强烈建议使用文档根目录之外的文件夹,因为htaccess并不总是可靠的。
不过,我们仍然需要用户或任何其他访客才能查看图像。因此,我们将使用php为他们检索图像:
<?php$uploaddir = 'uploads/';$name = $_GET['name']; // Assuming the file name is in the URL for this examplereadfile($uploaddir.$name);?>
第二个问题:本地文件包含攻击
尽管我们的脚本目前已经相当安全,但是我们不能认为服务器没有其他漏洞。一个常见的安全漏洞称为 本地文件包含 。为了解释这一点,我需要添加一个示例代码:
<?phpif(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;<?phpif(!empty($_POST['upload']) && !empty($_FILES['image']) && $_FILES['image']['error'] == 0)) { $uploaddir = 'uploads/'; function tempnam_sfx($path, $suffix){ do { $file = $path."/".mt_rand().$suffix; $fp = @fopen($file, 'x'); } while(!$fp); fclose($fp); return $file; } $verifyimg = getimagesize($_FILES['image']['tmp_name']); $pattern = "#^(image/)[^sn<]+$#i"; if(!preg_match($pattern, $verifyimg['mime']){ die("only image files are allowed!"); } $uploadfile = tempnam_sfx($uploaddir, ".tmp"); if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) { $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()); } $query = 'INSERT INTO uploads (name, original_name, mime_type) VALUES (:name, :oriname, :mime)'; $db->prepare($query); $db->bindParam(':name', basename($uploadfile)); $db->bindParam(':oriname', basename($_FILES['image']['name'])); $db->bindParam(':mime', $_FILES['image']['type']); 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;$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());}$query = 'SELECt name, original_name, mime_type FROM uploads WHERe id=:id';$db->prepare($query);$db->bindParam(':id', $id);try { $db->execute(); $result = $db->fetch(PDO::FETCH_ASSOC);}catch(PDOException $e){ die("Error!: " . $e->getMessage());}$newfile = $result['original_name'];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类,该类应该使所有人都可以更轻松地安全地处理网站访问者上传的图像。该类可一次处理单个和多个文件,并为您提供其他功能,例如显示,下载和删除图像。
只需阅读README.txt并按照说明进行操作即可。
开源
图像安全类项目现在也可以在我的Github个人资料上找到。这样其他人(您吗?)可以为该项目做出贡献,并使它成为每个人都喜欢的图书馆。



