文件上传的前世今生

从开始到现在,文件上传其实都是一项正常功能。其最大的好处就是方便了文件类型信息的交互与传递,大大减轻了现代办公压力。但是,它也有一个悖端,那就是,如果是不怀好意的人上传了不应该上传的内容,就会造成结果的不可预料。这样,就衍生成了一个严重的安全威胁。所以,我们要对文件上传内容进行过滤。

先来看一下什么是上传表单

<form action="up.php" enctype="multipart/form-data" method="post">Add File: <input name="file" type="file" value="" />
<input type="submit" value="Submit" /></form>&nbsp;

<?php if(isset($_POST)) { foreach($_FILES as $k=>$v) {
foreach ($v as $k=>$v)
{
                  echo '<pre>';
          echo $k.'----'.$v;
          echo '</pre>';
}
}

}

得到的结果为

name—-aserasfsadfdf.txt

type—-text/plain

tmp_name—-D:\test\tmp\phpA677.tmp

error—-0

size—-377

依次分别为包含后缀的文件名,文件类型,上传时产生的临时文件名,文件上传状态,文件大小。但是,这只是把上传文件打印出来,还没有真正实现上传,要想实现上传,还得用到一个函数 move_uploaded_file , 说到上传,不得不还要说2种文件上传判断方式

1. 黑名单判断。

2. 白名单判断。

黑名单判断就相当于如果恶意ext在array中,那么返回false,但是这样存在一个问题,恶意不可能包含所有文件类型,所以这里只要传任意一个在array中不存在的文件类型,就会直接导致验证被绕过,比如说,我在array中的是 cer,aspx,jsp,jspx。 此时如果上传前没有对大小写进行转换,那么,传一个.ASPX, 验证将会被绕过。所以,更多的时候,上传采用的验证类型一般是以白名单为主,那么什么是白名单呢? 认识的可以传,不认识的统统不可以。

黑名单判断实例

<?php

if(!isset($_FILES['file']))
{
    echo '请选择上传文件';
}
else
{
    if(isset($_POST))
               {
                 $tmp_file = $_FILES['file']['tmp_name'];
                 $name = $_FILES['file']['name'];
                 $type = $_FILES['file']['type'];
                 $file_ext= substr( $name, strrpos( $name, '.' ) + 1);
                 $dirname = $_SERVER['DOCUMENT_ROOT'].'/uploads/';
                 $dirname .=basename($_FILES['file']['name']);
                 $ext = array('php','jsp','asp');
            if(in_array($file_ext,$ext) || !move_uploaded_file($_FILES['file']['tmp_name'],$dirname))
            {
          echo '失败!';
            }
           else
              {
               echo '成功!';
               }
     }
    else
    {
        echo '上传错误!';
    }
       
}

可以看到, 表面上对后缀进行了过滤,事实上稍微改变一下大小写,或者传一个array中不存在的类型,该方式就被绕过了。这也是很多网站被入侵的一个原因。所以更多时候使用白名单,在说白名单之前,黑名单真的是一定可被绕过吗? 答案肯定是不一定的。这个可以后面再说, 先再来看一下白名单。

<?php

if(!isset($_FILES['file']))
{
    echo '请选择上传文件';
}
else
{
    if(isset($_POST))
    {
     $tmp_file = $_FILES['file']['tmp_name'];
     $name = $_FILES['file']['name'];
     $type = $_FILES['file']['type'];
     $file_ext= substr( $name, strrpos( $name, '.' ) + 1);
     $dirname = $_SERVER['DOCUMENT_ROOT'].'/uploads/';
     $dirname .=basename($_FILES['file']['name']);
     $ext = array('jpg','png','txt');
     if(in_array(strtolower($file_ext),$ext) && move_uploaded_file($_FILES['file']['tmp_name'],$dirname))
     {
       echo '成功!';
     }
     else
      {
       echo '失败!';
      }
    }
    else
    {
        echo '上传错误!';
    }
       
}

使用非array中文件类型上传,大写后缀均会上传失败, 原因是使用大写文件上传时,遇到strtolower均会被转换成小写,大写被破功,非array中的话,因为只有在array中搜索到了ext才会返回true, 没有搜索到就根本不会进入上传执行流程。 这里有个题外话,曾经有人问过我上传漏洞怎么补,我答:array一个ext数组不在数组中不让它传啊。 结果对方说,你这不是还是黑名单吗?希望阁下看到我的文章后能够明白。 其实如果想再安全点, 还可以加上对 $_FILES[‘file’][‘type’]的判断,如果是多类型文件上传,根据文件类型的不同再做出不同的判断,比如遇到图像后使用内置图像压缩函数对图像进行压缩等。 前面说到了黑名单绕过的疑问,这里重提一下,黑名单真是那么一无是处吗? 可以看一下

     $dirname = $_SERVER['DOCUMENT_ROOT'].'/uploads/';
     $dirname .=basename($_FILES['file']['name']);

从这里能看的出来, 上传后的路径是根据当前绝对路径和带后缀的文件名拼接而来的,所以这里如果对文件进行重命名,只要文件名在控制范围,无论黑名单怎么写,都不太可能造成意外结果。但这只是强词夺理,原因是白名单也一样。

比如

move_uploaded_file($_FILES['file']['tmp_name'],$dirname.'.'.md5(time('Y-m-d',round(0,9999999))))

无论怎么传,后缀都是随机数, 正常的情况下随机数不会被解析。再保险一点,filename可以只取文件名不取后缀。但是这样依然是存在问题的。 如果没有采用绝对路径,依然存在绕过的可能。 所以还是要对文件类型进行验证的。再一个,采用array判断方式也是存在问题的,前面说过,它存在意外返回true的可能,所以保险点的做法还是采用$ext == ‘JPG’ 进行比对。比如

strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png"

但是,如果服务端软件出现了漏洞,该验证依然会被打破,这也是为什么没有绝对安全的原因。综上所述,处理文件上传一般采用以下几种组合办法。

1. 前端判断,后端校验(JSON一样要确认)。

2. 上传目录去除解析或者上传到一个专用的子域。

3. 配置文件里配置安全目录。

还有一个要注意的是,很多人可能记起了过滤要采用白名单,但是他们把白名单过滤写到了前端,我想说的是,这样做是错误的,因为客户端内容是可以修改的。没有检查逻辑和错别字。

此条目发表在php security分类目录,贴了, , , , , 标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。 必填项已用*标注