创建一个可以上传文件的页面

EndlessLethe原创文章,转载请注明: 转载自小楼吹彻玉笙寒

本文链接地址: 创建一个可以上传文件的页面

前言

本文用以记录我创建一个文件上传页面的经历,解释了相关的html、http和安全的一些问题。

表单

<html>
<body>

<form action="upload_file.php" method="post"
enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>

</body>
</html>

这是一份用于上传的一个.php页面。标签form声明一个表单,它的action属性作为url,规定当提交表单时向何处发送表单数据。而在使用包含文件上传控件的表单时,必须使enctype属性为multipart/form-data,避免编码导致文件损坏。1

而下面的input和label标签都在form标签内。显然一个form标签需要至少一个type为submit的input标签来提交表单和一个type为text或其他的input标签来让用户输入。

input标签的type属性定义了这个object搜集用户信息方式,而后面可选的name属性规定了默认显示的字符串。比如说,当type=”text”时,浏览器就会生成一个textView,我们就可以在其中输入字符,以待提交给服务器。而=”submit”时就会为我们生成一个按钮,来提交表单。2

label标签通过for属性将自身与对应id的控件绑定。这里我们绑定了id=”file”的input控件。每当我们点击lable的内容时,我们就相当于点击了对应的控件。3

指向的脚本

这里指向的脚本不仅有可能是php,也有可能是asp等等。我们可以把”upload_file.php”写成这样:4 5

<?php
if ($_FILES["file"]["error"] > 0) {
  echo "Error: " . $_FILES["file"]["error"] . "<br />";
  }
else {
  echo "Upload: " . $_FILES["file"]["name"] . "<br />";
  echo "Type: " . $_FILES["file"]["type"] . "<br />";
  echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
  echo "Stored in: " . $_FILES["file"]["tmp_name"];
  }
?>

或者

<?php
header('Content-Type: text/plain');
print_r($_FILES);
?>

我更喜欢第二种,所以也就以第二种来讲解php的输出了。假如我通过以上表单上传一个logo到www.360weboy.me/upload.php

Array
(
    [file] => Array
        (
            [name] => boy.jpg
            [type] => image/jpeg
            [tmp_name] => D:\xampp\tmp\php1168.tmp
            [error] => 0
            [size] => 11490
        )

)

print_r函数先是打印$_FILES的类型为array。
然后打印了$_FILES里唯一的一个成员,名叫”file”的array,其中包括了五个成员。6
– $_FILES[“file”][“name”] – 被上传文件的名称
– $_FILES[“file”][“type”] – 被上传文件的类型
– $_FILES[“file”][“size”] – 被上传文件的大小,以字节计
– $_FILES[“file”][“tmp_name”] – 存储在服务器的文件的临时副本的名称
– $_FILES[“file”][“error”] – 由文件上传导致的错误代码。有0,1,3,4四种。7

PHP程序中,需要处理的上传数据保存在全局数组$_FILES(超全局数组之一 8)中。$_FILES将HTML表单中每一个input标签(除submit)上传文件的索引都以”id-长度为5的数组”的形式储存起来。

移动

我们已经成功接收到文件了(php已经把文件存放在临时服务器)。现在我们考虑做的就是把文件从临时服务器移动到文件夹而已,可以使用php的默认函数move_uploaded_file来实现。

<?php
$tmp_filename = $_FILES['file']['tmp_name'];
$filename = '/path/to/file.txt';
//这个路径应该是通过一些确保安全的步骤生成的,或者使用"name"对应的
if (move_uploaded_file(tmp_filename, $filename)) {
    /* $temp_filename 保存在临时目录中的上传文件, 然后成功将其保存到对应目录下的file.txt文件中. */
}
?>

我们已经实现文件的接受与保存了。从某种意义上,我们的功能已经完全实现。但是文件上传是获得服务器权限最快最直接的方式,我们不得不考虑一些安全问题。

简单的安全措施——控制上传类型

我们已经可以接受文件并转存了。但我们并不希望有恶意的请求破坏我们的网站。所以在移动之前我们需要添加一点小小的步骤。9

<?php
if (($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/pjpeg")) {
    //do something
}
else {
  echo "Invalid file";
}
?>

上面的代码简单的对类型做了检查。不过,我更喜欢下面的方式:

$allow_mimes = array(
    'image/png' => '.png',
    'image/x-png' => '.png',
    'image/gif' => '.gif',
    'image/jpeg' => '.jpg',
    'image/pjpeg' => '.jpg'
);

$image = $_FILES['file'];

if(!array_key_exists($image['type'], $allow_mimes )) {
    die('对不起, 你上传的文件格式不准确;我们只接受图片文件.');
}

这是以白名单的方式列出我们能接受的对象(显然,因为最小权限原则,我们不应该设置黑名单)。

残酷的现实!Content-Type并不可靠

刚刚我们是通过$_FILES的[“name”]来判断文件格式的。但是…当我们打开php官方的说明,我们就会发现,官方并没有保证这一字段的安全性,反而声明了它的不可靠。10 原因在于这个字段来源于浏览器header中的Content-Type属性。

我们看一下下面这个表单:11

POST /example.php HTTP/1.1
Host: 127.0.0.1
User-Agent: PEAR HTTP_Request class ( http://pear.php.net/ )
Content-Type: multipart/form-data; boundary=HTTP_Request_4893a7ce3361284d0e2ce39f2fdfa922
Connection: close
Accept-Encoding: gzip
Content-Length: 816

--HTTP_Request_4893a7ce3361284d0e2ce39f2fdfa922
Content-Disposition: form-data; name="Foo1"

bar
--HTTP_Request_4893a7ce3361284d0e2ce39f2fdfa922
Content-Disposition: form-data; name="Foo2"

1
--HTTP_Request_4893a7ce3361284d0e2ce39f2fdfa922
Content-Disposition: form-data; name="mail.gif"; filename="mail.gif"
Content-Type: image/gif

GIF89a    雁雂g櫰易疼嫖徵攒鸮d愇圊bi層泓毅逖蓊厢飃cび桕宿鹫骢脏镌屣冁褓潼劂钪狒厌鮢n曍膂袖笥泗孟吡惺]m囐觎!?     ,       `?俙亼,k抎?Va3R-_J?[`?罃H?`Q ?勉h6?瞐l4I?`嶕癷2翧?"   OKK;( b eW

c8 i,J  ?9, )f  ~刓np
 殒??A?+D`P*?P? `C?皺@
 刄 /?`? 锑`#?,pU,碌>?€ 艍@亷p? ;
--HTTP_Request_4893a7ce3361284d0e2ce39f2fdfa922

表单先声明了分割多个input标签上传的boundary为”4893a7ce3361284d0e2ce39f2fdfa922″。这里有三个input上传。前两个为text,所以直接显示为”bar”和”1″。最后一个为file,所以浏览器添加Content-Type来标记上传的文件类型。

我们可以在这里查看Content-Type和对应的文件格式。但是,这因为是可以被更改的表单,所以并不可靠。

更进一步的安全措施

避免Content-Type被修改,我们截取文件的文件名,再添加图片后缀名。上述代码对正常的上传的图片也不会有任何负面影响(相当于没有操作)。12

// 获取略去后缀名的文件名:
$filename = substr($image['name'], 0, strrpos($image['name'], '.'));

// 添加后缀名
$filename .= $allow_mimes[$image['type']];

我们需要检查下$_FILES全局数组中的tmp_name和size。为了确保tmp_name指向的文件确实是刚刚用户在客户端上传的文件,而不是指向的类似/etc/passwd,而导致/etc/passwd的文件被复制到可以被访问的文件夹下。可以使用php中的函数is_uploaded_file()来进行下判断:

<?php
$filename = $_FILES['attachment']['tmp_name'];
if (is_uploaded_file($filename)) {
    /* 是一个上传的文件. */
}
?>

最后我们可以用 filesize()来校验文件的大小:13

$tmp_filename = $_FILES['attachment']['tmp_name'];

 if (is_uploaded_file($tmp_filename)) {
   $size = filesize($tmp_filename);
 }

总结

汇总一下所有的代码:

<?php
$allow_mimes = array(
    'text/plain' => '.txt'
);
header('Content-Type: text/plain');
//print_r($_FILES);

$upload = $_FILES['file'];
$tmp_filename = $upload['tmp_name'];

if (is_uploaded_file($tmp_filename)) {
    $size = filesize($tmp_filename);
}
else {
    die('Error!');
}

if(!array_key_exists($upload['type'], $allow_mimes )) {
    die('对不起, 你上传的文件格式不准确;我们只接受txt文件.');
}

if(size > 4096) {
    die('对不起,您上传的文件太大!');
}

// 获取略去后缀名的文件名:
$filename = substr($upload['name'], 0, strrpos($upload['name'], '.'));
//echo $filename;

// 添加后缀名
$filename .= $allow_mimes[$upload['type']];
$filename = "/somewhere/" . $filename;
//echo $filename;

//if (move_uploaded_file($tmp_filename, $filename)) {
//    /* $temp_filename 保存在临时目录中的上传文件, 然后成功将其保存到对应目录下的attachment.txt文件中. */
//}
?>

服务器端的检查最好使用白名单过滤的方法,这样能防止大小写等方式的绕过,同时还需对%00截断符进行检测,对HTTP包头的content-type也和上传文件的大小也需要进行检查。14
文件上传漏洞可以说是日常渗透测试用得最多的一个漏洞,因为用它获得服务器权限最快最直接。在这篇文章中也不可能将文件上传漏洞的方方面面都讲到,否则失去了主次。“文件上传漏洞”的话题,我们下次再见吧。

发表评论

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