請注意,在呼叫 move_uploaded_file() 之前呼叫此函式並不是必要的,因為 move_uploaded_file() 已經執行了完全相同的檢查。它沒有提供額外的安全性。只有當您嘗試將上傳的檔案用於移動到新位置以外的其他用途時,才需要使用此函式。
參考
https://github.com/php/php-src/blob/master/ext/standard/basic_functions.c#L5796
(PHP 4 >= 4.0.3, PHP 5, PHP 7, PHP 8)
is_uploaded_file — 判斷檔案是否透過 HTTP POST 上傳
如果名為 filename
的檔案是透過 HTTP POST 上傳的,則返回 true
。這有助於確保惡意使用者沒有試圖欺騙腳本處理它不應該處理的檔案,例如 /etc/passwd。
如果任何使用上傳檔案的操作都有可能將其內容洩露給使用者,甚至是同一系統上的其他使用者,那麼這類檢查就尤為重要。
為了正常運作,is_uploaded_file() 函式需要一個像 $_FILES['userfile']['tmp_name'] 這樣的參數,也就是上傳檔案在用戶端機器上的暫存檔名。使用用戶端機器上的檔案名稱 $_FILES['userfile']['name'] 是無效的。
filename
要檢查的檔案名稱。
範例 #1 is_uploaded_file() 範例
<?php
if (is_uploaded_file($_FILES['userfile']['tmp_name'])) {
echo "檔案 ". $_FILES['userfile']['name'] ." 上傳成功。\n";
echo "顯示內容\n";
readfile($_FILES['userfile']['tmp_name']);
} else {
echo "可能為檔案上傳攻擊:";
echo "檔名 '". $_FILES['userfile']['tmp_name'] . "'。";
}
?>
請注意,在呼叫 move_uploaded_file() 之前呼叫此函式並不是必要的,因為 move_uploaded_file() 已經執行了完全相同的檢查。它沒有提供額外的安全性。只有當您嘗試將上傳的檔案用於移動到新位置以外的其他用途時,才需要使用此函式。
參考
https://github.com/php/php-src/blob/master/ext/standard/basic_functions.c#L5796
補充 nicoSWD 關於此函式的說明。
任何使用暫存檔 $_FILES[]['tmp_name'] 的腳本都應該呼叫此函式。
在任何腳本被修改為使用 unlink()、rename() 或以 move_uploaded_file() 以外的方式修改檔案的情況下,都不會檢查上傳的檔案。
同樣地,大多數檔案操作在 PHP 中都有快取,因此在 `move_uploaded_file` 之前執行 `is_uploaded_file` 對效能的影響應該極小,因為後者通常會使用快取的結果。
無論如何,安全性優勢大於微秒級的效能差異,而且應該在應用程式一開始取得 `$_FILES` 陣列時就全面使用。雖然目前可能沒有立即性的問題,但程式碼會不斷演進,這個事實可能很快就會改變。
從 PHP 4.2.0 開始,與其自動假設上傳失敗的檔案是檔案攻擊,您可以使用與檔案上傳相關聯的錯誤代碼來檢查上傳失敗的原因。此錯誤代碼儲存在 `userfile` 陣列中(例如:`$HTTP_POST_FILES['userfile']['error']`)。
以下是一個 switch 的範例
if (is_uploaded_file($userfile)) {
// 在此包含將暫存檔案複製到最終位置的程式碼...
}else{
switch($HTTP_POST_FILES['userfile']['error']){
case 0: // 沒有錯誤;可能是檔案攻擊!
echo "您的上傳發生問題。";
break;
case 1: // 上傳的檔案超過 php.ini 中的 upload_max_filesize 指令
echo "您嘗試上傳的檔案太大。";
break;
case 2: // 上傳的檔案超過 html 表單中指定的 MAX_FILE_SIZE 指令
echo "您嘗試上傳的檔案太大。";
break;
case 3: // 上傳的檔案只有部分上傳
echo "您嘗試上傳的檔案只有部分上傳。";
break;
case 4: // 沒有上傳任何檔案
echo "您必須選擇要上傳的圖片。";
break;
default: // 預設錯誤,以防萬一! :)
echo "您的上傳發生問題。";
break;
}
此外,透過測試檔案上傳陣列的「name」元素,您可以篩選掉不需要的檔案類型(.exe、.zip、.bat 等)。以下是一個可以在測試檔案是否已上傳之前新增的篩選器範例
// 拒絕所有 .exe、.com、.bat、.zip、.doc 和 .txt 檔案
if(preg_match("/.exe$|.com$|.bat$|.zip$|.doc$|.txt$/i", $HTTP_POST_FILES['userfile']['name'])){
exit("您無法上傳此類型的檔案。");
}
// 如果檔案未被篩選器拒絕,則正常繼續
if (is_uploaded_file($userfile)) {
/*其餘程式碼*/
我從未在任何地方看到有人提到這點,但在 Windows 上,`$filename` *確實* 區分大小寫。
這表示雖然可能已上傳 C:\Windows\TEMP\php123.tmp,但 C:\Windows\Temp\php123.tmp 並沒有。
我發現這一點是因為我在檔名上使用了 `realpath()`,它「修正」了大小寫(我的 Temp 資料夾是大小寫混合的,而不是全大寫 - 感謝 Vista)。
總之,問題在於 PHP 使用 `%TEMP%` 來決定上傳檔案的目的地,而 `%TEMP%` 使用的是全大寫版本的路徑。將其改為使用大小寫混合的版本,然後重新啟動 Apache 就解決了問題。
剛剛又看了一遍我發布的內容,發現了一些大大小小的錯誤。這就是我在喝完咖啡之前發布的下場。這個版本應該會更好(也就是說,它一開始就應該可以運作)。
<?php
default: //預設錯誤處理,以防萬一! :)
echo "上傳檔案時發生問題。";
$err_msg = "無法辨識的檔案 POST 錯誤: ".$HTTP_POST_FILES['userfile']['error'];
if (!(strpos($err_msg, "\n") === false)) {
$err_lines = explode("\n", $err_msg);
foreach ($err_lines as $msg) {
error_log($msg, 0);
}
} else {
error_log($err_msg, 0);
}
break;
?>
這是我的檔案處理程式碼,希望對大家有幫助
首先是一個處理檔案上傳的類別
<?php
define('UPLOAD_PATH', 'upload/');
define('MAXIMUM_FILESIZE', '10485760'); //10 MB
class FileHandler
{
private $file_types = array('xls', 'xlsx');
private $files = null;
private $filename_sanitized = null;
private $filename_original = null;
public function __construct($files)
{
$this->files = $files;
}
public function setFileTypes($fileTypes = array())
{
$this->file_types = $fileTypes;
return $this;
}
public function setFileNameOriginal($filename)
{
$this->filename_original = $filename;
}
public function fileNameOriginal()
{
return $this->filename_original;
}
public function sanitize($cursor = 0)
{
$this->setFileNameOriginal($this->files['name'][$cursor]);
$safe_filename = preg_replace(
array("/\s+/", "/[^-\.\w]+/"),
array("_", ""),
trim($this->fileNameOriginal()));
$this->filename_sanitized = md5($safe_filename.time()).$safe_filename;
return $this;
}
public function fileSize($cursor = 0)
{
return $this->files['size'][$cursor];
}
public function extensionValid()
{
$fileTypes = implode('|', $this->file_types);
$rEFileTypes = "/^\.($fileTypes){1}$/i";
if(!preg_match($rEFileTypes, strrchr($this->filename_sanitized, '.')))
throw new Exception('No se pudo encontrar el tipo de archivo apropiado');
return $this;
}
public function isUploadedFile($cursor)
{
if(!is_uploaded_file($this->files['tmp_name'][$cursor]))
{
throw new Exception("No se obtuvo la carga del archivo");
}
}
public function saveUploadedFile($cursor)
{
if(!move_uploaded_file ($this->files['tmp_name'][$cursor],UPLOAD_PATH.$this->filename_sanitized))
throw new Exception("No se consiguió guardar el archivo");
}
public function fileNameSanitized()
{
return $this->filename_sanitized;
}
public function uploadFile($cursor = 0)
{
$this->isUploadedFile($cursor);
if ($this->sanitize($cursor)->fileSize($cursor) <= MAXIMUM_FILESIZE)
{
$this->extensionValid()->saveUploadedFile($cursor);
}
else
{
throw new Exception("El archivo es demasiado grande.");
}
return $name;
}
}
?>
接下來是使用該類別的部分程式碼
<?php
//表單已提交並偵測到
if ($form_submited == 1)
{
try
{
//我假設輸入檔案是:
/*
<input id="<?php echo EXCEL_FILE;?>[]" name="<?php echo EXCEL_FILE;?>[]" type="file"/>
其中 EXCEL_FILE 是常數:
define('EXCEL_FILE', 'excel_file');
*/
$file = new FileHandler($_FILES['excel_file']);
$inputFileName = $file->uploadFile()->fileNameSanitized(); // 要讀取的檔案
...
}
catch(Exception $e)
{
die('載入檔案 "'.($file->fileNameOriginal()).'": 發生錯誤 '.$e->getMessage());
}
}
?>
提供的範例沒有如預期般運作
function is_uploaded_file($filename) {
if (!$tmp_file = get_cfg_var('upload_tmp_dir')) {
$tmp_file = dirname(tempnam('', ''));
}
$tmp_file .= '/' . basename($filename);
/* 使用者可能在 php.ini 中使用了尾端斜線... */
return (ereg_replace('/+', '/', $tmp_file) == $filename);
}
它只適用於 4 或 5 kb 以下的檔案,其他檔案的大小會自動變成 0 位元組。所以這裡一定有什麼問題。內建的 is_uploaded_file() 函式運作良好。
如果你的 $_FILES 和 $_POST 是空的,這可能是由於
- php.ini 中 post_max_size 設定的限制
- php.ini 中 upload_max_filesize 設定的限制
遺憾的是,第一個限制不會在 $_FILES['error'] 中以錯誤碼的形式回報。
要在 Windows 上執行這個範例,你必須加一行,把反斜線換成斜線。例如: $filename = str_replace ("\\", "/", $filename);
還有,就像有人提到的,將 $HTTP_POST_FILES 設為全域變數是個好主意...
<pre>
/* 測試使用者上傳的檔案。 */
function is_uploaded_file($filename)
{
global $HTTP_POST_FILES;
if (!$tmp_file = get_cfg_var("upload_tmp_dir")) {
$tmp_file = dirname(tempnam("", ""));
}
$tmp_file .= "/" . basename($filename);
/* 使用者可能在 php.ini 中使用了尾端斜線... */
// Windows 平台修正
$filename = str_replace ("\\", "/", $filename);
return (ereg_replace("/+", "/", $tmp_file) == $filename);
}
</pre>
如果檔案無法上傳,且 $_FILE 陣列是空的,而你的程式碼看起來沒問題,那就檢查 php.ini 檔案。file_uploads 選項應該要設定為 'On' 才能允許檔案上傳。開啟它並重新啟動 Apache 才能生效。
關於 info at metaltoad dot net 的評論
@ 2003年2月19日 04:03
<?php
// ... 等等等等...
preg_match("/.exe$|.com$|.bat$|.zip$|.doc$|.txt$/i", $HTTP_POST_FILES['userfile']['name']))
// ... 等等等等...
?>
這樣不行。它會執行,但結果不正確。
你應該要對 preg 函式中的 . (點) 進行跳脫字元處理,
也要對 PHP 中的 $ (錢字號) 進行跳脫字元處理,或者使用
單引號字串...
語法應該要像這樣 (更簡潔俐落)
<?php
// ... 等等等等...
preg_match('/\\.(exe|com|bat|zip|doc|txt)$/i', $_FILES['userfile']['name']))
// ... 等等等等...
?>
關於 topcat 建議的修改,我有點猶豫。我不喜歡向使用者顯示可能洩漏過多資訊的錯誤訊息 (或顯示我沒有處理該特定錯誤)。但我又想知道何時發生了落入預設情況的錯誤,這樣我才能修正我的程式碼。我通常會把這些錯誤寫入錯誤日誌,就像這樣修改 metaltoad 的文章 (考慮到 error_log 不太能處理多行錯誤的可能性)
<?php
default: //預設錯誤,以防萬一! :)
echo "上傳檔案時發生問題。";
$err_msg = "無法辨識的檔案 POST 錯誤: ".$HTTP_POST_FILES['userfile']['error'];
if ((strpos($err_msg, "\n") === 0) {
$err_lines = explode("\n", $err_msg);
foreach ($err_lines as $msg) {
error_log($msg, 0);
}
} else {
error_log($err_msg, 0)
}
break;
?>