PHP编码安全之六: 文件上传安全

本文内容参考自《PHP安全之道》。

文件上传漏洞的危害

在PHP项目中, 提供上传功能并在服务器端未对上传的文件格式进行合理的校验是存在巨大风险的。如果恶意攻击者利用上传漏洞上传一些 webshell,则可能完全控制整个网站程序, 执行系统命令(如删除系统文件), 获取数据库连接字符串进行数据库操作等危险操作。

文件上传漏洞

以下是一个不安全的文件上传后端代码 (upload.php):

$upload_dir = 'uploads/';
$upload_file = $upload_dir . basename($_FILES['file']['name']);
if(move_upload_file($_FILES['file']['tmp_name'], $upload_file)){
    echo '文件上传成功!';
}else{
    echo '错误: 文件上传失败!';
}

前端html:

<form name="upload" action="upload.php" method="post" encrypt="multipart/form-data">
  请选择文件:
  <input type="file" name="file" />
  <input type="submit" name="submit" value="上传" />
</form>

这是一个简单的文件上传模块. 其中由用户上传文件, 如果上传成功, 则保存文件的路径为: http://服务器路径/uploads/文件名称

如果攻击者上传一个如下内容的hacker.php脚本文件到服务器:

<?php
	system($_GET['shell']);

则攻击者就可以通过该文件进行url请求 http://服务器路径/uploads.php?hacker.php?shell=ls%20-al, 从而可以执行任何shell命令.

检查文件类型防止上传漏洞

上面的演示代码很简单, 没有做任何的上传限制。 如果要限制,通常的做法是限制文件上传类型(MIME)

if($_FILES['file']['tye'] != 'images/gif'){
    die('请上传正确的文件类型!');
}
$upload_dir = 'uploads/';
$upload_file = $upload_dir . basename($_FILES['file']['name']);
if(move_upload_file($_FILES['file']['tmp_name'], $upload_file)){
    echo '文件上传成功!';
}else{
    echo '错误: 文件上传失败!';
}

如果攻击者试图上传 shell.php, 会被系统拒绝. 这里简单的通过文件类型检测阻止了非法文件上传。

但是只是简单的进行文件类型MIME检查也是不够的, 攻击者通过修改POST数据包中的Content-type: text/plain 为 Content-type: image/gif 即可骗过系统。

检查文件扩展名称防止上传漏洞

除了检测MIME类型,常用的方法是基于黑白名单验证所传文件的扩展名称是否符合要求。

以下代码通过黑名单方式对文件类型进行限制:

$ext_black_list = ['.php', '.phtml', '.php3', '.php4'];//黑名单
$upload_dir = 'uploads/';
$file_name = $_FILES['file']['name'];
$upload_file = $upload_dir . basename($file_name);
$ext = substr($file_name, -4);//扩展名
if(in_array($ext, $ext_black_list)){
    die('请选择正确的文件类型');
}
if(move_uploaded_file($_FILES['file']['tmp_name'], $upload_file)){
    echo '文件上传成功!';
}else{
    echo '错误: 文件上传失败!';
}

以下代码通过白名单方式对文件类型进行限制:

$ext_white_list = ['.jpg', '.gif', '.png'];//白名单
$upload_dir = 'uploads/';
$file_name = $_FILES['file']['name'];
$upload_file = $upload_dir . basename($file_name);
$ext = substr($file_name, -4);//扩展名
if(!in_array($ext, $ext_white_list)){
    die('请选择正确的文件类型');
}
if(move_uploaded_file($_FILES['file']['tmp_name'], $upload_file)){
    echo '文件上传成功!';
}else{
    echo '错误: 文件上传失败!';
}

从黑名单和白名单两种不同的验证方法来看,白名单方式要比黑名单更安全, 但是采取白名单还是不够的。

Apache存在一个扩展名解析漏洞(apahce2.4下仍然存在): 如果一个文件有多个扩展名时, 如果最后的扩展名未定义, 就会解析前一个扩展名, 比如 hacker.php.2018 会解析成.php扩展名, 从而该文件被当做脚本执行。如果使用黑名单机制, 很明显无法防御此种攻击,但是白名单机制就可以。

文件上传漏洞的综合防护

我们不能期望只通过一种安全手段就能阻止非法文件上传, 应该同时综合运用文件类型检测、后缀名检测、黑白名单机制、使用随机文件名、上传文件目录禁止执行权限等多种方法同时进行。

/**
 * 生成随机字符串
 * @param int $len
 * @return string
 */
function getRandomString(int $len){
    $chars = array_merge(range('a', 'z'), range('A', 'Z'), range(0, 9));
    $char_len = count($chars) - 1;
    shuffle($chars); //将数组打乱
    $rt = '';
    for($i=0; $i < $len; $i++){
        $rt .= $chars[mt_rand(0, $char_len)];
    }
    return $rt;
}

$ext_white_list = ['.jpg', '.gif', '.png'];//后缀名白名单
$file_name = $_FILES['file']['name']; //原文件名
$ext = substr($file_name, -4);//扩展名
if(!in_array($ext, $ext_white_list)){
    die('请选择正确的文件类型');
}

$mime_white_list = ['image/gif', 'image/png', 'image/jpeg']; //MIME白名单
if(!in_array($_FILES['file']['type'], $mime_white_list)){
    die('请选择正确的文件类型');
}

$upload_dir = '/tmp/uploads/';//将上传的文件放到项目目录之外
$upload_file = $upload_dir . getRandomString(20). $ext; // 使用随机文件名
if(move_uploaded_file($_FILES['file']['tmp_name'], $upload_file)){
    echo '文件上传成功!';
}else{
    echo '错误: 文件上传失败!';
}

除了在代码中阻止上传漏洞外, 还需要在项目部署时对上传目录进行安全设置: 禁止文件的执行权限, 把该目录的所有文件当做静态资源处理。

当用户上传文件到服务器时保存时, 一定要使用随机文件名进行存储, 并保证所存储的扩展名合法。保证文件名的唯一性,也保证了存储的安全性,可以防止上传文件非法扩展的解析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章