PHP – 上传图片代码实现与安全加固

代码来自DVWA

PHP实现上传需要知道的东西

1.Form标签enctype属性

登录页面的 form 标签

<form action="login.php" method="post">

上传页面的 form 标签

<form action="upload.php" method="post" enctype="multipart/form-data">
  • 表单中的 enctype 是用于设置表单的 MIME 编码
  • 默认情况下 enctype 编码的格式为 application/x-www-form-unlencoded
  • application/x-www-form-unlencoded 会在发送前编码所有字符,所以不适合传输文件
  • multipart/form-data 不会对字符编码,使用了这种编码且提交方式为 POST 才能完整的传递文件数据

2.MAX_FILE_SIZE隐藏字段

<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
  • MAX_FILE_SIZE 隐藏字段(单位为字节)必须放在文件输入字段之前,其值为接收文件的最大尺寸。这是对浏览器的一个建议,PHP 也会检查此项
  • 在浏览器端可以简单绕过此设置,因此不要指望用此特性来阻挡大文件
  • 鉴于友好性最好还是在表单中加上此项目,因为它可以避免用户在花时间等待上传大文件之后才发现文件过大上传失败的麻烦

3.全局数组 $_FILES

$_FILES["file"]["name"] - 被上传文件的名称
$_FILES["file"]["type"] - 被上传文件的类型
$_FILES["file"]["size"] - 被上传文件的大小,以字节计
$_FILES["file"]["tmp_name"] - 存储在服务器的文件的临时副本的名称
$_FILES["file"]["error"] - 由文件上传导致的错误代码

4.php.ini中有关文件上传的设置

file_uploads          是否允许通过HTTP上传文件的开关。默认为ON即是开
upload_tmp_dir        文件上传至服务器上存储临时文件的地方,如果没指定就会用系统默认的临时文件夹
upload_max_filesize   即允许上传文件大小的最大值。默认为2M
post_max_size         指通过表单POST给PHP的所能接收的最大值,包括表单里的所有值。默认为8M

5.关于php上传文件的一些常用函数

file_exists()           检查文件或目录是否存在
move_uploaded_file()    将上传的文件移动到新位置

iconv()                 字符编码互转
str_replace()           字符串替换(更改文件名,防重名)

is_writable()           判断给定的文件名是否可写
is_uploaded_file()      判断文件是否是通过 HTTP POST 上传的

getimagesize()          检查是否为图片文件(其他类型的文件就算后缀名改了也能被检测到)

PHP上传代码

没有任何限制

实现了上传功能,但是没有做任何限制
可以上传任意文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<html>
<head>
<title>UPLOAD</title>
<meta charset="utf-8">
<?php
if (isset($_POST['upload'])){
#设置文件存放位置
$path = "./images/";
$path .= basename($_FILES['uploaded']['name']);

#保存上传文件并反馈
if (!move_uploaded_file($_FILES['uploaded']['tmp_name'], $path)){
echo '<pre>Your image was not uploaded.</pre>';
}
else{
echo "<pre>{$path} successfully uploaded!</pre>";
}
}
?>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
Choose an image to upload:
<input name="uploaded" type="file" /><br />
<input type="submit" name="upload" value="upload" />
</body>
</html>

对文件类型和文件大小进行限制

限制了文件的类型为jpeg或png并且文件大小为100000字节以下
但是通过burpsuite等工具抓包改包绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<html>
<head>
<title>UPLOAD</title>
<meta charset="utf-8">
<?php
if (isset($_POST['upload'])){
#设置文件存放位置
$path = "./images/";
$path .= basename($_FILES['uploaded']['name']);

$file_type = $_FILES['uploaded']['type'];
$file_size = $_FILES['uploaded']['size'];

#对文件类型和文件大小进行限制
if (($file_type=="image/jpeg"||$file_type=="image/png")&&$file_size < 100000]){
#保存上传文件并反馈
if (!move_uploaded_file($_FILES['uploaded']['tmp_name'], $path)){
echo '<pre>Your image was not uploaded.</pre>';
}
else{
echo "<pre>{$path} successfully uploaded!</pre>";
}
}
else{
echo "<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>";
}
}
?>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
Choose an image to upload:
<input name="uploaded" type="file" /><br />
<input type="submit" name="upload" value="upload" />
</body>
</html>

对文件类型、文件名后缀和文件大小进行限制,并判断文件是否为图片

限制了文件的类型为jpeg或png并且文件大小为100000字节以下
利用文件名的后缀进行验证,并判断临时文件的文件头是否为图片文件
可以通过%00截断绕过文件名后缀检测,用图片马绕过文件头检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<html>
<head>
<title>UPLOAD</title>
<meta charset="utf-8">
<?php
if (isset($_POST['upload'])){
#设置文件存放位置
$path = "./images/";
$path .= basename($_FILES['uploaded']['name']);

$file_name = $_FILES['uploaded']['name'];
$file_type = $_FILES['uploaded']['type'];
$file_size = $_FILES['uploaded']['size'];
$file_ftmp = $_FILES['uploaded']['tmp_name'];
$file_exte = substr($file_name,strrpos($file_name,'.')+1);

#对文件类型、文件名后缀和文件大小进行限制,并判断文件是否为图片
if (($file_type=="image/jpeg"||$file_type=="image/png")&&($file_exte=="jpep"||$file_exte=="png")&&$file_size < 00000&&getimagesize($file_exte)){
#保存上传文件并反馈
if (!move_uploaded_file($_FILES['uploaded']['tmp_name'], $path)){
echo '<pre>Your image was not uploaded.</pre>';
}
else{
echo "<pre>{$path} successfully uploaded!</pre>";
}
}
else{
echo "<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>";
}
}
?>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
Choose an image to upload:
<input name="uploaded" type="file" /><br />
<input type="submit" name="upload" value="upload" />
</body>
</html>

对文件类型、文件名后缀和文件大小进行限制,并判断文件是否为图片,重写图片

限制了文件的类型为jpeg或png并且文件大小为100000字节以下
利用文件名的后缀进行验证,并判断临时文件的文件头是否为图片文件
对上传文件进行了重命名(为md5值,导致%00截断无法绕过过滤规则)
对文件的内容作了严格的检查,导致无法上传含有恶意脚本的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<html>
<head>
<title>UPLOAD</title>
<meta charset="utf-8">
<?php
if (isset($_POST['upload'])){

$file_name = $_FILES['uploaded']['name'];
$file_type = $_FILES['uploaded']['type'];
$file_size = $_FILES['uploaded']['size'];
$file_ftmp = $_FILES['uploaded']['tmp_name'];
$file_exte = substr($file_name,strrpos($file_name,'.')+1);

#设置文件存放位置
$path = "./images/";
$target_file = md5(uniqid().$file_name).'.'.$file_exte;
$temp_file =((ini_get('upload_tmp_dir')=='')?(sys_get_temp_dir()):(ini_get('upload_tmp_dir')));
$temp_file .= DIRECTORY_SEPARATOR.md5(uniqid().$file_name).'.'.$file_exte;

#对文件类型、文件名后缀和文件大小进行限制,并判断文件是否为图片
if (($file_type=="image/jpeg"||$file_type=="image/png")&&($file_exte=="jpeg"||$file_exte=="png")&&$file_size < 200000&&getimagesize($file_ftmp)){

#删除源数据 重新编码图像
if ($file_type == 'images/jpeg'){
$img=imagecreatefromjpeg($file_ftmp);
imagejpeg($img,$temp_file,100);
}
else{
$img=imagecreatefrompng($file_ftmp);
imagepng($img,$temp_file,9);
}
imagedestroy($img);

if (rename($temp_file,(getcwd().DIRECTORY_SEPARATOR.$path.$target_file))){
echo "<pre><a href='${path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";
}
else{
echo "<pre>Your image was not uploaded.</pre>";
}

if(file_exists($temp_file)){
unlink($temp_file);
}
}
else{
echo "<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>";
}
}
?>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="200000" />
Choose an image to upload:
<input name="uploaded" type="file" /><br />
<input type="submit" name="upload" value="upload" />
</body>
</html>

上传漏洞防御

客户端检测

  • 使用JS对上传图片检测,包括文件大小、文件拓展名、文件类型等

服务端检测

  • 对文件大小、文件路径、文件拓展名、文件类型、文件内容检测
  • 对文件重命名

其他限制

  • 上传目录设置不可执行权限

php后缀

php语言除了可以解析php后缀,还可以解析php2,php3,php4,php5