完整的安全图像上传脚本 [英] Full Secure Image Upload Script

查看:16
本文介绍了完整的安全图像上传脚本的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我不知道这是否会发生,但我会尝试.

I don't know if this going to happen, but I will try it.

在过去的一个小时里,我研究了图片上传的安全性.我了解到有很多功能可以测试上传.

For past hour I did research on image upload safety. I learned that there a lot of functions to test the upload.

在我的项目中,我需要确保上传图片的安全.也可能有非常大的数量,它可能需要大量带宽,因此购买 API 不是一种选择.

In my project, I need to be safe with images uploaded. There also may be a really big amount of it and it may require a lot of bandwidth, so buying an API is not an option.

所以我决定获取一个完整的 PHP 脚本来真正安全地上传图片.我还认为它会对那里的许多人有所帮助,因为不可能找到真正安全的.但是我不是php方面的专家,所以添加一些功能对我来说真的很头疼,所以我会请求这个社区帮助创建一个真正安全的图片上传的完整脚本.

So I decided to get a full PHP script for REALLY secure image upload. I also think it will help for many of people out there, because it's impossible to find really secure one. But I am not expert in php, so it's really headache for me to add some functions, so I will ask for this community help to create one full script of REALLY secure image upload.

这里有非常棒的主题(然而,他们只是告诉我们需要什么来完成这个技巧,而不是如何做到这一点,正如我所说,我不是 PHP 的高手,所以我无法自己做这一切):PHP图片上传安全检查清单https://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form

Really great topics about that are here (however, they are just telling what is needed to do the trick, but not how to do this, and as I said I am not a master on PHP, so I am not able to do this all by myself): PHP image upload security check list https://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form

总而言之,他们告诉我们这是安全图像上传所需要的(我将引用上述页面):

In summary, they are telling that this is what is needed for security image upload (I will quote from the above pages):

  • 使用 .httaccess 禁止 PHP 在上传文件夹内运行.
  • 如果文件名包含字符串php",则不允许上传.
  • 仅允许扩展名:jpg、jpeg、gif 和 png.
  • 仅允许图像文件类型.
  • 禁止使用两种文件类型的图像.
  • 更改图像名称.上传到子目录而不是根目录.

还有:

  • 使用 GD(或 Imagick)重新处理图像并保存处理后的图像.所有其他人对黑客来说都只是有趣无聊"
  • 正如 rr 所指出的,使用 move_uploaded_file() 进行任何上传"
  • 顺便说一下,您可能希望对上传文件夹进行严格限制.这些地方是许多漏洞的黑暗角落之一
    发生.这适用于任何类型的上传和任何编程
    语言/服务器.检查
    https://www.owasp.org/index.php/Unrestricted_File_Upload
  • 级别 1:检查扩展名(扩展名文件以)
  • 级别 2:检查 MIME 类型 ($file_info = getimagesize($_FILES['image_file']; $file_mime = $file_info['mime'];)
  • 第 3 级:读取前 100 个字节并检查它们是否有以下范围内的任何字节:ASCII 0-8、12-31(十进制).
  • 第 4 级:检查标题中的幻数(文件的前 10-20 个字节).您可以从以下位置找到一些文件头字节这里:
    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

这是其中的很大一部分,但还不是全部.(如果您知道更多有助于使上传更安全的内容,请分享.)

Here's a big part of it, but still that's not all. (If you know something more which could help to make the upload even safier, please share.)

这就是我们现在得到的

  • 主要 PHP:

  • Main 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;
}

  • 还有表格:

  • And the form:

    <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>
    

  • 所以,我要问的是通过发布代码片段来帮助我(和其他所有人)使这个图像上传脚本变得超级安全.或者通过共享/创建添加了所有片段的完整脚本.

    So, what I am asking is to help by posting snippets of codes which will help me (and everyone else) to make this Image Upload Script to make super secure. Or by sharing/creating a full script with all the snippets added.

    推荐答案

    当您开始处理安全的图像上传脚本时,有很多事情需要考虑.现在我远不是这方面的专家,但我过去曾被要求开发这个.我将介绍我在这里经历的整个过程,以便您可以跟随.为此,我将从一个非常基本的 html 表单和处理文件的 php 脚本开始.

    When you start working on a secure image upload script, there are many things to consider. Now I'm no where near an expert on this, but I've been asked to develop this once in the past. I'm gonna walk through the entire process I've been through here so you can follow along. For this I'm gonna start with a very basic html form and php script that handles the files.

    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 脚本.无论我们多么努力,我们都无法阻止攻击者尝试上传他们不应该上传的内容.所以我们所有的安全都必须在服务器端完成.

    First problem: File types
    Attackers don't have to use the form on your website to upload files to your server. POST requests can be intercepted in a number of ways. Think about browser addons, proxies, Perl scripts. No matter how hard we try, we can't prevent an attacker from trying to upload something they're not supposed to. So all of our security has to be done serverside.

    第一个问题是文件类型.在上面的脚本中,攻击者可以上传他们想要的任何东西,例如 php 脚本,然后点击直接链接来执行它.因此,为了防止这种情况发生,我们实施了内容类型验证:

    The first problem is file types. In the script above an attacker could upload anything they want, like a php script for example, and follow a direct link to execute it. So to prevent this, we implement Content-type verification:

    <?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.";
    }
    ?>
    

    不幸的是,这还不够.正如我之前提到的,攻击者可以完全控制请求.没有什么可以阻止他/她修改请求标头并简单地将内容类型更改为image/png".因此,与其仅仅依赖 Content-type 标头,还不如验证上传文件的内容.这就是 php GD 库派上用场的地方.使用 getimagesize(),我们将使用 GD 库处理图像.如果它不是图像,这将失败,因此整个上传将失败:

    Unfortunately this isn't enough. As I mentioned before, the attacker has full control over the request. Nothing will prevent him/her from modifying the request headers and simply change the Content type to "image/png". So instead of just relying on the Content-type header, it would be better to also validate the content of the uploaded file. Here's where the php GD library comes in handy. Using getimagesize(), we'll be processing the image with the GD library. If it isn't an image, this will fail and therefor the entire upload will fail:

    <?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 解释器不会处理图像.这就是为什么添加文件扩展名白名单也不够安全.

    We're still not there yet though. Most image file types allow text comments added to them. Again, nothing prevents the attacker from adding some php code as a comment. The GD library will evaluate this as a perfectly valid image. The PHP interpreter would completely ignore the image and run the php code in the comment. It's true that it depends on the php configuration which file extensions are processed by the php interpreter and which not, but since there are many developers out there that have no control over this configuration due to the use of a VPS, we can't assume the php interpreter won't process the image. This is why adding a file extension white list isn't safe enough either.

    对此的解决方案是将图像存储在攻击者无法直接访问文件的位置.这可能在文档根目录之外或在受 .htaccess 文件保护的目录中:

    The solution to this would be to store the images in a location where an attacker can't access the file directly. This could be outside of the document root or in a directory protected by a .htaccess file:

    order deny,allow
    deny from all
    allow from 127.0.0.1
    

    在与其他一些 PHP 程序员交谈后,我强烈建议使用文档根目录之外的文件夹,因为 htaccess 并不总是可靠的.

    After talking with some other PHP programmers, I highly suggest using a folder outside of the document root, because htaccess isn't always reliable.

    我们仍然需要用户或任何其他访问者能够查看图像.所以我们将使用 php 为他们检索图像:

    We still need the user or any other visitor to be able to view the image though. So we'll use php to retrieve the image for them:

    <?php
    $uploaddir = 'uploads/';
    $name = $_GET['name']; // Assuming the file name is in the URL for this example
    readfile($uploaddir.$name);
    ?>
    

    第二个问题:本地文件包含攻击
    尽管我们的脚本现在相当安全,但我们不能假设服务器没有其他漏洞.一个常见的安全漏洞称为本地文件包含.为了解释这一点,我需要添加一个示例代码:

    Second problem: Local file inclusion attacks
    Although our script is reasonably secure by now, we can't assume the server doesn't suffer from other vulnerabilities. A common security vulnerability is known as Local file inclusion. To explain this I need to add an example code:

    <?php
    if(isset($_COOKIE['lang'])) {
       $lang = $_COOKIE['lang'];
    } elseif (isset($_GET['lang'])) {
       $lang = $_GET['lang'];
    } else {
       $lang = 'english';
    }
    
    include("language/$lang.php");
    ?>
    

    在本例中,我们讨论的是多语言网站.网站语言不是被认为是高风险"的东西.信息.我们尝试通过 cookie 或 GET 请求获取访问者的首选语言,并基于它包含所需的文件.现在考虑当攻击者输入以下网址时会发生什么:

    In this example we're talking about a multi language website. The sites language is not something considered to be "high risk" information. We try to get the visitors preferred language through a cookie or a GET request and include the required file based on it. Now consider what will happen when the attacker enters the following url:

    www.example.com/index.php?lang=../uploads/my_evil_image.jpg

    PHP 将包含攻击者上传的文件,绕过他们无法直接访问文件的事实,我们又回到了原点.

    PHP will include the file uploaded by the attacker bypassing the fact that they can't access the file directly and we're back at square one.

    这个问题的解决方案是确保用户不知道服务器上的文件名.相反,我们将使用数据库更改文件名甚至扩展名以跟踪它:

    The solution to this problem is to make sure the user doesn't know the filename on the server. Instead, we'll change the file name and even the extension using a database to keep track of it:

    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
    <]+$#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!");
        }
    }
    ?>
    

    所以现在我们已经完成了以下工作:

    So now we've done the following:

    • 我们创建了一个安全的地方来保存图像
    • 我们已经使用 GD 库处理了图像
    • 我们已检查图像 MIME 类型
    • 我们重命名了文件名并更改了扩展名
    • 我们已在数据库中保存了新文件名和原始文件名
    • 我们还在数据库中保存了 MIME 类型

    我们仍然需要能够向访问者显示图像.我们简单地使用我们数据库的 id 列来做到这一点:

    We still need to be able to display the image to visitors. We simply use the id column of our database to do this:

    <?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']);
    ?>
    

    借助此脚本,访问者将能够查看图像或使用其原始文件名下载图像.但是,他们无法直接访问您服务器上的文件,也无法欺骗您的服务器为他/她访问该文件,因为他们无法知道这是哪个文件.他们也不能强制您的上传目录,因为它根本不允许任何人访问除了服务器本身之外的目录.

    Thanks to this script the visitor will be able to view the image or download it with its original filename. However, they can't access the file on your server directly nor will they be able to fool your server to access the file for him/her as they has no way of knowing which file it is. They can't brute force your upload directory either as it simply doesn't allow anyone to access the directory except the server itself.

    我的安全图片上传脚本到此结束.

    And that concludes my secure image upload script.

    我想补充一点,我没有在此脚本中包含最大文件大小,但您应该很容易自己做到这一点.

    I'd like to add that I didn't include a maximum file size into this script, but you should easily be able to do that yourself.

    ImageUpload 类
    由于这个脚本的高需求,我编写了一个 ImageUpload 类,它应该可以让你们所有人更容易安全地处理网站访问者上传的图像.该类可以同时处理单个和多个文件,并为您提供显示、下载和删除图像等附加功能.

    ImageUpload Class
    Due to the high demand of this script, I've written an ImageUpload class that should make it a lot easier for all of you to securely handle images uploaded by your website visitors. The class can handle both single and multiple files at once, and provides you with additional features like displaying, downloading and deleting images.

    由于代码太大,无法在此处发布,您可以在此处从 MEGA 下载该类:

    Since the code is simply to large to post here, you can download the class from MEGA here:

    下载图片上传类

    只需阅读 README.txt 并按照说明操作即可.

    Just read the README.txt and follow the instructions.

    开源
    Image Secure 类项目现在也可以在我的 Github 个人资料中找到.这样其他人(您?)就可以为该项目做出贡献,并使其成为适合所有人的绝佳图书馆.

    Going Open Source
    The Image Secure class project is now also available on my Github profile. This so that others (you?) can contribute towards the project and make this a great library for everyone.

    这篇关于完整的安全图像上传脚本的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆