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 '錯誤: 文件上傳失敗!';
}

除了在代碼中阻止上傳漏洞外, 還需要在項目部署時對上傳目錄進行安全設置: 禁止文件的執行權限, 把該目錄的所有文件當做靜態資源處理。

當用戶上傳文件到服務器時保存時, 一定要使用隨機文件名進行存儲, 並保證所存儲的擴展名合法。保證文件名的唯一性,也保證了存儲的安全性,可以防止上傳文件非法擴展的解析。

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