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

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

问题描述

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

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



在我的项目中,我需要安全的上传图片。也可能有一个非常大的数量,它可能需要很多的带宽,所以购买一个API不是一个选项。



所以我决定得到一个完整的PHP脚本真正安全的图像上传。我也认为这对很多人来说都是有帮助的,因为找到真正安全的人是不可能的。但是我并不擅长使用PHP,因此添加一些函数让我很头疼,所以我会要求这个社区帮助创建一个真正安全的图片上传的完整脚本。



关于这方面的真正重要的话题在这里(然而,他们只是告诉做什么需要做的伎俩,而不是如何做到这一点,正如我说我不是PHP的高手,所以我不能这一切都由我自己):
PHP图像上传安全检查列表
https://security.stackexchange.com/questions / 32852 / risk-of-a-php-image-upload-form

总之,他们说这是安全形象所需要的上传(我会从上面的页面引用):



  • 禁用PHP在上传文件夹内运行。 httaccess。

  • 如果文件名包含字符串php,则不允许上传。

  • 只允许扩展名:jpg,jpeg,gif和png。 >仅允许图像文件类型。
  • 不允许使用两种文件类型的图像。
  • 更改图像名称。上传到一个不是根目录的子目录。



另外:


这里有很大一部分,但还不是全部。 (如果你知道更多的东西可以帮助上传更安全,请分享。)



这就是我们现在所了解的

  • 主要PHP:

     函数uploadFile($ file_field = null,$ check_image = false,$ random_name = false){

    //配置节
    //设置文件上传路径
    $ path ='uploads /'; //以结尾斜线
    //设置最大文件大小(以字节为单位)
    $ max_size = 1000000;
    //设置默认文件扩展白名单
    $ whitelist_ext = array('jpeg','jpg','png','gif');
    //设置默认文件类型白名单
    $ whitelist_type = array('image / jpeg','image / jpg','image / png','image / gif');

    //验证
    //创建一个数组来保存任何输出
    $ out = array('error'=> null);
    $ b $ if(!$ file_field){
    $ out ['error'] [] =请指定一个有效的表单字段名称;

    $ b $ if(!$ path){
    $ out ['error'] [] =请指定一个有效的上传路径;


    if(count($ out ['error'])> 0){
    return $ out; ($!$($ _FILES [$ file_field]))&&($ _FILES [$ file_field($ file_field)$


    //确保有一个文件
    ] ['error'] == 0)){

    //获取文件名
    $ file_info = pathinfo($ _ FILES [$ file_field] ['name']);
    $ name = $ file_info ['filename'];
    $ ext = $ file_info ['extension'];
    $ b $ //检查文件是否有正确的扩展名
    if(!in_array($ ext,$ whitelist_ext)){
    $ out ['error'] [] =无效的文件延期;


    检查文件的类型是
    if(!in_array($ _ FILES [$ file_field] [type],$ whitelist_type)){
    $ out ['error'] [] =文件类型无效;


    检查文件不是太大
    if($ _FILES [$ file_field] [size]> $ max_size){
    $ out ['error'] [] =文件太大;

    $ b $ //如果$ check image设置为true
    if($ check_image){
    if(!getimagesize($ _ FILES [$ file_field] [' tmp_name'])){
    $ out ['error'] [] =上传的文件不是有效的图片;


    $ b //创建完整的文件名,包括路径
    if($ random_name){
    //生成随机文件名
    $ tmp = str_replace(array('。',''),array('',''),microtime());

    if(!$ tmp || $ tmp ==''){
    $ out ['error'] [] =文件必须有名字;
    }
    $ newname = $ tmp。'。'。$ ext;
    } else {
    $ newname = $ name。'。'。$ ext;


    检查文件是否已经存在于服务器
    if(file_exists($ path。$ newname)){
    $ out ['error'] [ ] =具有此名称的文件已经存在;


    if(count($ out ['error'])> 0){
    //文件没有正确验证
    return $ out;
    }

    if(move_uploaded_file($ _ FILES [$ file_field] ['tmp_name'],$ path。$ newname)){
    // Success
    $ out ['filepath'] = $ path;
    $ out ['filename'] = $ newname;
    返回$ out;
    } else {
    $ out ['error'] [] =Server Error!;
    }

    } else {
    $ out ['error'] [] =没有文件上传;
    返回$ out;



    $ b if(isset($ _ POST ['submit'])){
    $ file = uploadFile('file',true ,真);
    if(is_array($ file ['error'])){
    $ message ='';
    foreach($ file ['error'] as $ msg){
    $ message。='< p>。$ msg。'< / p>';
    }
    } else {
    $ message =文件上传成功$ newname;
    }
    echo $ message;

    $ / code $ / pre
    $ li $ b $ b

     < form action =<?php echo $ _SERVER ['PHP_SELF'];?> method =postenctype =multipart / form-dataname =form1id =form1> 
    < input name =filetype =fileid =imagee/>
    < input name =submittype =submitvalue =Upload/>
    < / form>




  • 通过张贴代码片段,这将帮助我(和其他人)使这个图像上传脚本,使超级安全。
    或者通过共享/创建一个包含所有代码片段的完整脚本。 当你开始一个安全图片上传脚本,有很多事情要考虑。现在我不是靠近这方面的专家,但我曾经被要求过去发展一次。我要走过我在这里经历的整个过程,所以你可以跟随。为此,我将从一个非常基本的html表单和php脚本开始处理这些文件。



    HTML表单:

     < form name =uploadaction =upload.phpmethod =POSTenctype =multipart / form-data> 
    选择要上传的图片:< input type =filename =image>
    < input type =submitname =uploadvalue =upload>
    < / form>

    PHP文件:

     <?php 
    $ uploaddir ='uploads /';

    $ uploadfile = $ uploaddir。基名($ _ FILES [图像] [名称]);
    $ b $ if(move_uploaded_file($ _ FILES ['image'] ['tmp_name'],$ uploadfile)){
    echoImage succeededfully uploaded。;
    } else {
    echo图片上传失败。
    }
    ?>

    第一个问题:文件类型

    攻击者不必使用您的网站上的表格上传文件到您的服务器。 POST请求可以通过多种方式截获。考虑浏览器插件,代理,Perl脚本。不管我们多努力,我们都无法阻止攻击者尝试上传他不应该的东西。所以我们所有的安全性都必须在服务器端完成。


    第一个问题是文件类型。在上面的脚本中,攻击者可以上传他想要的任何东西,比如php脚本,然后直接执行。因此,为了防止这种情况,我们实施了 Content-type验证

     <?php 
    if($ _ FILES ['image'] ['type']!=image / png){
    echo只允许使用PNG图像!
    出口;
    }

    $ uploaddir ='uploads /';

    $ uploadfile = $ uploaddir。基名($ _ FILES [图像] [名称]);
    $ b $ if(move_uploaded_file($ _ FILES ['image'] ['tmp_name'],$ uploadfile)){
    echoImage succeededfully uploaded。;
    } else {
    echo图片上传失败。
    }
    ?>

    不幸的是这还不够。正如我之前提到的,攻击者完全可以控制请求。没有什么会阻止他/她修改请求标题,只是将内容类型改为image / png。因此,不要只依赖Content-type头,最好还要验证上传文件的内容。这里是PHP GD库方便的地方。使用 getimagesize(),我们将用GD库处理图像。如果它不是一个图像,这将失败,因此,整个上传将失败:

    pre $ <?php
    $ verifyimg = getimagesize($ _ FILES ['image'] ['tmp_name']);
    $ b $ if($ verifyimg ['mime']!='image / png'){
    echo只允许使用PNG图片!
    出口;
    }

    $ uploaddir ='uploads /';

    $ uploadfile = $ uploaddir。基名($ _ FILES [图像] [名称]);
    $ b $ if(move_uploaded_file($ _ FILES ['image'] ['tmp_name'],$ uploadfile)){
    echoImage succeededfully uploaded。;
    } else {
    echo图片上传失败。
    }
    ?>

    尽管如此,大多数图像文件类型允许将文本注释添加到它们。同样,没有什么能够阻止攻击者添加一些php代码作为评论。 GD图书馆将评估这是一个完全有效的图像。 PHP解释器会完全忽略图像,并在注释中运行php代码。这是真的,这取决于PHP配置哪些文件扩展名由php解释器处理,哪些不是,但由于有许多开发人员由于使用VPS而无法控制此配置,所以我们不能假定PHP解释器不会处理图像。这就是为什么添加一个文件扩展名白名单也不够安全。



    解决方案是将图像存储在攻击者不能直接访问文件。这可能在文档根目录之外,或者在一个由.htaccess文件保护的目录中:

     命令拒绝,允许
    拒绝所有
    允许从127.0.0.1

    编辑:与其他PHP程序员,我强烈建议在文档根目录之外使用一个文件夹,因为htaccess并不总是可靠。



    我们仍然需要用户或任何其他访问者能够查看图像虽然。因此,我们将使用PHP来检索他们的图像:

     <?php 
    $ uploaddir ='uploads /';
    $ name = $ _GET ['name']; //假设文件名在这个例子的URL中
    readfile($ uploaddir。$ name);
    ?>

    第二个问题:本地文件包含攻击

    虽然我们的脚本现在是相当安全的,但是我们不能假定服务器不会遭受其他的漏洞。一个常见的安全漏洞被称为本地文件包含。为了解释这一点,我需要添加一个示例代码:

    pre $ 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请求获取访客首选语言,并根据它包含所需的文件。现在,considder会在攻击者输入以下URL时发生什么:

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

    PHP会包含攻击者上传的文件,绕过他无法直接访问文件的事实,我们又回到了原来的位置。



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

      CREATE TABLE`uploads ($ B $ id` INT 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'])&& amp ; $ _FILES ['image'] ['error'] == 0)){

    $ uploaddir ='uploads /';
    $ b $ *生成随机文件名和扩展名* /
    函数tempnam_sfx($ path,$ suffix){
    do {
    $ file = $ path。/ 。.mt_rand()$后缀;
    $ fp = @fopen($ file,'x');
    }
    while(!$ fp);

    fclose($ fp);
    返回$文件;

    $ b / *带GD库的过程映像* /
    $ verifyimg = getimagesize($ _ FILES ['image'] ['tmp_name']);
    $ b $ *确保MIME类型是图片* /
    $ pattern =#^(image /)[^ \\\\\\\\\\\\\\\\\\\\\'
    $ b $ 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)){

    / *使用PDO设置数据库连接* /
    $ dbhost =localhost;
    $ dbuser =;
    $ dbpass =;
    $ dbname =;

    //设置DSN
    $ dsn ='mysql:host ='。$ dbhost。'; dbname ='。$ dbname;

    //设置选项
    $ options = array(
    PDO :: ATTR_PERSISTENT => true,
    PDO :: ATTR_ERRMODE => PDO :: ERRMODE_EXCEPTION
    );

    try {
    $ db =新的PDO($ dsn,$ dbuser,$ dbpass,$ options);
    }
    catch(PDOException $ e){
    die(Error !:)。 $ E->的getMessage());
    $ *
    $ * / *设置查询* /
    $查询='插入上传(名称,original_name,mime_type)VALUES(:name,:oriname,:mime)';
    $ b $ *准备查询* /
    $ db-> prepare($ query);
    $ b / *绑定参数* /
    $ db-> bindParam(':name',basename($ uploadfile));
    $ db-> bindParam(':oriname',basename($ _ FILES ['image'] ['name']));
    $ db-> bindParam(':mime',$ _FILES ['image'] ['type']);
    $ b $ *执行查询* /
    尝试{
    $ db-> execute();

    catch(PDOException $ e){
    //删除上传的文件
    unlink($ uploadfile);

    die(Error !:。$ e-> getMessage());
    }
    } else {
    die(Image Upload failed!);
    }
    }
    ?>

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

    我们已经创建了一个安全的地方来保存图像

  • 我们已经使用GD库处理图像

  • 我们检查了图片的MIME类型

  • 我们重命名了文件名并更改了扩展名

  • 新的和原始的文件名在我们的数据库中

  • 我们也保存了我们的数据库中的MIME类型


    <我们仍然需要能够向访客展示图像。

     <?php 

    $我们只需使用数据库的id列uploaddir ='上传/';
    $ id = 1;

    设置与PDO的数据库连接* /
    $ dbhost =localhost;
    $ dbuser =;
    $ dbpass =;
    $ dbname =;

    //设置DSN
    $ dsn ='mysql:host ='。$ dbhost。'; dbname ='。$ dbname;

    //设置选项
    $ options = array(
    PDO :: ATTR_PERSISTENT => true,
    PDO :: ATTR_ERRMODE => PDO :: ERRMODE_EXCEPTION
    );

    尝试{
    $ db =新的PDO($ dsn,$ dbuser,$ dbpass,$ options);

    catch(PDOException $ e){
    die(Error !:。$ e-> getMessage());

    $ b / *设置查询* /
    $ query ='SELECT name,original_name,mime_type FROM uploads WHERE id =:id';
    $ b $ *准备查询* /
    $ db-> prepare($ query);
    $ b $ *绑定参数* /
    $ db-> bindParam(':id',$ id);
    $ b $ *执行查询* /
    尝试{
    $ db-> execute();
    $ result = $ db-> fetch(PDO :: FETCH_ASSOC);

    catch(PDOException $ e){
    die(Error !:。$ e-> getMessage());
    }

    / *获取原始文件名* /
    $ newfile = $ result ['original_name'];
    $ b $ *发送头文件到访问者* /
    头('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 Class

    由于这个脚本的高要求,我写了一个ImageUpload类,让所有人都能安全地处理您的网站访问者上传的图片。该类可以同时处理单个文件和多个文件,并为您提供其他功能,如显示,下载和删除图像。


    由于代码只是大到在这里张贴,你可以在这里下载MEGA的课程:


    下载ImageUpload Class

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


    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.

    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.

    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.

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

    • 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

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

    THIS IS WHAT WE GOT NOW

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

    解决方案

    When you start working on a secure image upload script, there are many things to considder. 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:

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

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

    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 (s)he isn't supposed to. So all of our security has to be done serverside.

    The first problem is file types. In the script above an attacker could upload anything (s)he wants, 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.";
    }
    ?>
    

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

    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.

    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
    

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

    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");
    ?>
    

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

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

    PHP will include the file uploaded by the attacker bypassing the fact that (s)he 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\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!");
        }
    }
    ?>
    

    So now we've done the following:

    • We've created a secure place to save the images
    • We've processed the image with the GD library
    • We've checked the image MIME type
    • We've renamed the file name and changed the extension
    • We've saved both the new and original filename in our database
    • We've also saved the MIME type in our database

    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, (s)he can't access the file on your server direcly nor will (s)he be able to fool your server to access the file for him/her as (s)he has no way of knowing which file it is. (S)he 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 Class
    Due to the high demand of this script, I've writting 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.

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

    Download ImageUpload Class

    Just read the README.txt and follow the instructions.

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

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