PHP 應用程序中的安全性包括遠程安全性和本地安全性。本文將揭示 PHP 開(kāi)發(fā)人員在實(shí)現具有這兩種安全性的 Web 應用程序時(shí)應該養成的習慣。
在提及安全性問(wèn)題時(shí),需要注意,除了實(shí)際的平臺和操作系統安全性問(wèn)題之外,您還需要確保編寫(xiě)安全的應用程序。在編寫(xiě) PHP 應用程序時(shí),請應用下面的七個(gè)習慣以確保應用程序具有最好的安全性:
- 驗證輸入
- 保護文件系統
- 保護數據庫
- 保護會(huì )話(huà)數據
- 保護跨站點(diǎn)腳本(Cross-site scripting,XSS)漏洞
- 檢驗表單 post
- 針對跨站點(diǎn)請求偽造(Cross-Site Request Forgeries,CSRF)進(jìn)行保護
驗證輸入
在提及安全性問(wèn)題時(shí),驗證數據是您可能采用的最重要的習慣。而在提及輸入時(shí),十分簡(jiǎn)單:不要相信用戶(hù)。您的用戶(hù)可能十分優(yōu)秀,并且大多數用戶(hù)可能完全按照期望來(lái)使用應用程序。但是,只要提供了輸入的機會(huì ),也就極有可能存在非常糟糕的輸入。作為一名應用程序開(kāi)發(fā)人員,您必須阻止應用程序接受錯誤的輸入。仔細考慮用戶(hù)輸入的位置及正確值將使您可以構建一個(gè)健壯、安全的應用程序。
雖然后文將介紹文件系統與數據庫交互,但是下面列出了適用于各種驗證的一般驗證提示:
- 使用白名單中的值
- 始終重新驗證有限的選項
- 使用內置轉義函數
- 驗證正確的數據類(lèi)型(如數字)
白名單中的值(White-listed value)是正確的值,與無(wú)效的黑名單值(Black-listed value)相對。兩者之間的區別是,通常在進(jìn)行驗證時(shí),可能值的列表或范圍小于無(wú)效值的列表或范圍,其中許多值可能是未知值或意外值。
在進(jìn)行驗證時(shí),記住設計并驗證應用程序允許使用的值通常比防止所有未知值更容易。例如,要把字段值限定為所有數字,需要編寫(xiě)一個(gè)確保輸入全都是數字的例程。不要編寫(xiě)用于搜索非數字值并在找到非數字值時(shí)標記為無(wú)效的例程。
保護文件系統
2000 年 7 月,一個(gè) Web 站點(diǎn)泄露了保存在 Web 服務(wù)器的文件中的客戶(hù)數據。該 Web 站點(diǎn)的一個(gè)訪(fǎng)問(wèn)者使用 URL 查看了包含數據的文件。雖然文件被放錯了位置,但是這個(gè)例子強調了針對攻擊者保護文件系統的重要性。
如果 PHP 應用程序對文件進(jìn)行了任意處理并且含有用戶(hù)可以輸入的變量數據,請仔細檢查用戶(hù)輸入以確保用戶(hù)無(wú)法對文件系統執行任何不恰當的操作。清單 1 顯示了下載具有指定名的圖像的 PHP 站點(diǎn)示例。
清單 1. 下載文件 <?php if ($_POST['submit'] == 'Download') { $file = $_POST['fileName']; header("Content-Type: application/x-octet-stream"); header("Content-Transfer-Encoding: binary"); header("Content-Disposition: attachment; filename=\"" . $file . "\";" ); $fh = fopen($file, 'r'); while (! feof($fh)) { echo(fread($fh, 1024)); } fclose($fh); } else { echo("<html><head><"); echo("title>Guard your filesystem</title></head>"); echo("<body><form id=\"myFrom\" action=\"" . $_SERVER['PHP_SELF'] . "\" method=\"post\">"); echo("<div><input type=\"text\" name=\"fileName\" value=\""); echo(isset($_REQUEST['fileName']) ? $_REQUEST['fileName'] : ''); echo("\" />"); echo("<input type=\"submit\" value=\"Download\" name=\"submit\" /></div>"); echo("</form></body></html>"); }
|
正如您所見(jiàn),清單 1 中比較危險的腳本將處理 Web 服務(wù)器擁有讀取權限的所有文件,包括會(huì )話(huà)目錄中的文件(請參閱 “保護會(huì )話(huà)數據”),甚至還包括一些系統文件(例如/etc/passwd)。為了進(jìn)行演示,這個(gè)示例使用了一個(gè)可供用戶(hù)鍵入文件名的文本框,但是可以在查詢(xún)字符串中輕松地提供文件名。
同時(shí)配置用戶(hù)輸入和文件系統訪(fǎng)問(wèn)權十分危險,因此最好把應用程序設計為使用數據庫和隱藏生成的文件名來(lái)避免同時(shí)配置。但是,這樣做并不總是有效。清單 2 提供了驗證文件名的示例例程。它將使用正則表達式以確保文件名中僅使用有效字符,并且特別檢查圓點(diǎn)字符:..。
清單 2. 檢查有效的文件名字符 function isValidFileName($file) { /* don't allow .. and allow any "word" character \ / */ return preg_match('/^(((?:\.)(?!\.))|\w)+$/', $file); }
|
保護數據庫2008年 4 月,美國某個(gè)州的獄政局在查詢(xún)字符串中使用了 SQL列名,因此泄露了保密數據。這次泄露允許惡意用戶(hù)選擇需要顯示的列、提交頁(yè)面并獲得數據。這次泄露顯示了用戶(hù)如何能夠以應用程序開(kāi)發(fā)人員無(wú)法預料的方法執行輸入,并表明了防御 SQL 注入攻擊的必要性。
清單 3 顯示了運行 SQL 語(yǔ)句的示例腳本。在本例中,SQL語(yǔ)句是允許相同攻擊的動(dòng)態(tài)語(yǔ)句。此表單的所有者可能認為表單是安全的,因為他們已經(jīng)把列名限定為選擇列表。但是,代碼疏忽了關(guān)于表單欺騙的最后一個(gè)習慣— 代碼將選項限定為下拉框并不意味著(zhù)其他人不能夠發(fā)布含有所需內容的表單(包括星號 [*])。
清單 3. 執行 SQL 語(yǔ)句 <html> <head> <title>SQL Injection Example</title> </head> <body> <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> <div><input type="text" name="account_number" value="<?php echo(isset($_POST['account_number']) ? $_POST['account_number'] : ''); ?>" /> <select name="col"> <option value="account_number">Account Number</option> <option value="name">Name</option> <option value="address">Address</option> </select> <input type="submit" value="Save" name="submit" /></div> </form> <?php if ($_POST['submit'] == 'Save') { /* do the form processing */ $link = mysql_connect('hostname', 'user', 'password') or die ('Could not connect' . mysql_error()); mysql_select_db('test', $link);
$col = $_POST['col'];
$select = "SELECT " . $col . " FROM account_data WHERE account_number = " . $_POST['account_number'] . ";" ; echo '<p>' . $select . '</p>';
$result = mysql_query($select) or die('<p>' . mysql_error() . '</p>');
echo '<table>'; while ($row = mysql_fetch_assoc($result)) { echo '<tr>'; echo '<td>' . $row[$col] . '</td>'; echo '</tr>'; } echo '</table>';
mysql_close($link); } ?> </body> </html>
|
因此,要形成保護數據庫的習慣,請盡可能避免使用動(dòng)態(tài) SQL 代碼。如果無(wú)法避免動(dòng)態(tài) SQL 代碼,請不要對列直接使用輸入。清單 4 顯示了除使用靜態(tài)列外,還可以向帳戶(hù)編號字段添加簡(jiǎn)單驗證例程以確保輸入值不是非數字值。
清單 4. 通過(guò)驗證和mysql_real_escape_string()提供保護 <html> <head> <title>SQL Injection Example</title> </head> <body> <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> <div><input type="text" name="account_number" value="<?php echo(isset($_POST['account_number']) ? $_POST['account_number'] : ''); ?>" /> <input type="submit" value="Save" name="submit" /></div> </form> <?php function isValidAccountNumber($number) { return is_numeric($number); }
if ($_POST['submit'] == 'Save') {
/* Remember habit #1--validate your data! */ if (isset($_POST['account_number']) && isValidAccountNumber($_POST['account_number'])) {
/* do the form processing */ $link = mysql_connect('hostname', 'user', 'password') or die ('Could not connect' . mysql_error()); mysql_select_db('test', $link);
$select = sprintf("SELECT account_number, name, address " . " FROM account_data WHERE account_number = %s;", mysql_real_escape_string($_POST['account_number'])); echo '<p>' . $select . '</p>';
$result = mysql_query($select) or die('<p>' . mysql_error() . '</p>');
echo '<table>'; while ($row = mysql_fetch_assoc($result)) { echo '<tr>'; echo '<td>' . $row['account_number'] . '</td>'; echo '<td>' . $row['name'] . '</td>'; echo '<td>' . $row['address'] . '</td>'; echo '</tr>'; } echo '</table>';
mysql_close($link); } else { echo "<span style=\"font-color:red\">" . "Please supply a valid account number!</span>";
} } ?> </body> </html>
|
本例還展示了mysql_real_escape_string()函數的用法。此函數將正確地過(guò)濾您的輸入,因此它不包括無(wú)效字符。如果您一直依賴(lài)于magic_quotes_gpc,那么需要注意它已被棄用并且將在 PHP V6 中刪除。從現在開(kāi)始應避免使用它并在此情況下編寫(xiě)安全的 PHP 應用程序。此外,如果使用的是 ISP,則有可能您的 ISP 沒(méi)有啟用magic_quotes_gpc。
最后,在改進(jìn)的示例中,您可以看到該 SQL語(yǔ)句和輸出沒(méi)有包括動(dòng)態(tài)列選項。使用這種方法,如果把列添加到稍后含有不同信息的表中,則可以輸出這些列。如果要使用框架以與數據庫結合使用,則您的框架可能已經(jīng)為您執行了 SQL驗證。確保查閱文檔以保證框架的安全性;如果仍然不確定,請進(jìn)行驗證以確保穩妥。即使使用框架進(jìn)行數據庫交互,仍然需要執行其他驗證。
保護會(huì )話(huà)默認情況下,PHP 中的會(huì )話(huà)信息將被寫(xiě)入臨時(shí)目錄??紤]清單 5 中的表單,該表單將顯示如何存儲會(huì )話(huà)中的用戶(hù) ID 和帳戶(hù)編號。
清單 5. 存儲會(huì )話(huà)中的數據 <?php session_start(); ?> <html> <head> <title>Storing session information</title> </head> <body> <?php if ($_POST['submit'] == 'Save') { $_SESSION['userName'] = $_POST['userName']; $_SESSION['accountNumber'] = $_POST['accountNumber']; } ?> <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> <div><input type="hidden" name="token" value="<?php echo $token; ?>" /> <input type="text" name="userName" value="<?php echo(isset($_POST['userName']) ? $_POST['userName'] : ''); ?>" /> <br /> <input type="text" name="accountNumber" value="<?php echo(isset($_POST['accountNumber']) ? $_POST['accountNumber'] : ''); ?>" /> <br /> <input type="submit" value="Save" name="submit" /></div> </form> </body> </html>
|
清單 6 顯示了 /tmp 目錄的內容。
清單 6. /tmp 目錄中的會(huì )話(huà)文件 -rw------- 1 _www wheel 97 Aug 18 20:00 sess_9e4233f2cd7cae35866cd8b61d9fa42b
|
正如您所見(jiàn),在輸出時(shí)(參見(jiàn)清單 7),會(huì )話(huà)文件以非常易讀的格式包含信息。由于該文件必須可由 Web 服務(wù)器用戶(hù)讀寫(xiě),因此會(huì )話(huà)文件可能為共享服務(wù)器中的所有用戶(hù)帶來(lái)嚴重的問(wèn)題。除您之外的某個(gè)人可以編寫(xiě)腳本來(lái)讀取這些文件,因此可以嘗試從會(huì )話(huà)中取出值。
清單 7. 會(huì )話(huà)文件的內容 userName|s:5:"ngood";accountNumber|s:9:"123456789";
|
| 存儲密碼 不 管是在數據庫、會(huì )話(huà)、文件系統中,還是在任何其他表單中,無(wú)論如何密碼都決不能存儲為純文本。處理密碼的最佳方法是將其加密存儲并相互比較加密的密碼。雖 然如此,在實(shí)踐中人們仍然把密碼存儲到純文本中。只要使用可以發(fā)送密碼而非重置密碼的 Web 站點(diǎn),就意味著(zhù)密碼是存儲在純文本中或者可以獲得用于解密的代碼(如果加密的話(huà))。即使是后者,也可以找到并使用解密代碼。 | |
您可以采取兩項操作來(lái)保護會(huì )話(huà)數據。第一是把您放入會(huì )話(huà)中的所有內容加密。但是正因為加密數據并不意味著(zhù)絕對安全,因此請慎重采用這種方法作為保護會(huì )話(huà)的惟一方式。備選方法是把會(huì )話(huà)數據存儲在其他位置中,比方說(shuō)數據庫。您仍然必須確保鎖定數據庫,但是這種方法將解決兩個(gè)問(wèn)題:第一,它將把數據放到比共享文件系統更加安全的位置;第二,它將使您的應用程序可以更輕松地跨越多個(gè) Web 服務(wù)器,同時(shí)共享會(huì )話(huà)可以跨越多個(gè)主機。
要實(shí)現自己的會(huì )話(huà)持久性,請參閱 PHP 中的session_set_save_handler()函數。使用它,您可以將會(huì )話(huà)信息存儲在數據庫中,也可以實(shí)現一個(gè)用于加密和解密所有數據的處理程序。清單 8 提供了實(shí)現的函數用法和函數骨架示例。您還可以在參考資料小節中查看如何使用數據庫。
清單 8.session_set_save_handler()函數示例 function open($save_path, $session_name) { /* custom code */ return (true); }
function close() { /* custom code */ return (true); }
function read($id) { /* custom code */ return (true); }
function write($id, $sess_data) { /* custom code */ return (true); }
function destroy($id) { /* custom code */ return (true); }
function gc($maxlifetime) { /* custom code */ return (true); }
session_set_save_handler("open", "close", "read", "write", "destroy", "gc");
|
針對 XSS 漏洞進(jìn)行保護XSS 漏洞代表 2007 年所有歸檔的 Web 站點(diǎn)的大部分漏洞(請參閱參考資料)。當用戶(hù)能夠把 HTML 代碼注入到您的 Web 頁(yè)面中時(shí),就是出現了 XSS 漏洞。HTML 代碼可以在腳本標記中攜帶 JavaScript代碼,因而只要提取頁(yè)面就允許運行 JavaScript。清單 9 中的表單可以表示論壇、維基、社會(huì )網(wǎng)絡(luò )或任何可以輸入文本的其他站點(diǎn)。
清單 9. 輸入文本的表單 <html> <head> <title>Your chance to input XSS</title> </head> <body> <form id="myFrom" action="showResults.php" method="post"> <div><textarea name="myText" rows="4" cols="30"></textarea><br /> <input type="submit" value="Delete" name="submit" /></div> </form> </body> </html>
|
清單 10 演示了允許 XSS 攻擊的表單如何輸出結果。
清單 10. showResults.php <html> <head> <title>Results demonstrating XSS</title> </head> <body> <?php echo("<p>You typed this:</p>"); echo("<p>"); echo($_POST['myText']); echo("</p>"); ?> </body> </html>
|
清單 11 提供了一個(gè)基本示例,在該示例中將彈出一個(gè)新窗口并打開(kāi) Google 的主頁(yè)。如果您的 Web 應用程序不針對 XSS 攻擊進(jìn)行保護,則會(huì )造成嚴重的破壞。例如,某個(gè)人可以添加模仿站點(diǎn)樣式的鏈接以達到欺騙(phishing)目的(請參閱參考資料)。
清單 11. 惡意輸入文本樣例 <script type="text/javascript">myRef = window.open('http://www.google.com','mywin', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0');</script>
|
要防止受到 XSS 攻擊,只要變量的值將被打印到輸出中,就需要通過(guò)htmlentities()函數過(guò)濾輸入。記住要遵循第一個(gè)習慣:在 Web 應用程序的名稱(chēng)、電子郵件地址、電話(huà)號碼和帳單信息的輸入中用白名單中的值驗證輸入數據。
下面顯示了更安全的顯示文本輸入的頁(yè)面。
清單 12. 更安全的表單 <html> <head> <title>Results demonstrating XSS</title> </head> <body> <?php echo("<p>You typed this:</p>"); echo("<p>"); echo(htmlentities($_POST['myText'])); echo("</p>"); ?> </body> </html>
|
針對無(wú)效 post 進(jìn)行保護表單欺騙是指有人把 post 從某個(gè)不恰當的位置發(fā)到您的表單中。欺騙表單的最簡(jiǎn)單方法就是創(chuàng )建一個(gè)通過(guò)提交至表單來(lái)傳遞所有值的 Web 頁(yè)面。由于Web 應用程序是沒(méi)有狀態(tài)的,因此沒(méi)有一種絕對可行的方法可以確保所發(fā)布數據來(lái)自指定位置。從 IP 地址到主機名,所有內容都是可以欺騙的。清單13 顯示了允許輸入信息的典型表單。
清單 13. 處理文本的表單 <html> <head> <title>Form spoofing example</title> </head> <body> <?php if ($_POST['submit'] == 'Save') { echo("<p>I am processing your text: "); echo($_POST['myText']); echo("</p>"); } ?> </body> </html>
|
清單 14 顯示了將發(fā)布到清單 13 所示表單中的表單。要嘗試此操作,您可以把該表單放到 Web 站點(diǎn)中,然后把清單 14 中的代碼另存為桌面上的 HTML 文檔。在保存表單后,在瀏覽器中打開(kāi)該表單。然后可以填寫(xiě)數據并提交表單,從而觀(guān)察如何處理數據。
清單 14. 收集數據的表單 <html> <head> <title>Collecting your data</title> </head> <body> <form action="processStuff.php" method="post"> <select name="answer"> <option value="Yes">Yes</option> <option value="No">No</option> </select> <input type="submit" value="Save" name="submit" /> </form> </body> </html>
|
表單欺騙的潛在影響是,如果擁有含下拉框、單選按鈕、復選框或其他限制輸入的表單,則當表單被欺騙時(shí)這些限制沒(méi)有任何意義??紤]清單 15 中的代碼,其中包含帶有無(wú)效數據的表單。
清單 15. 帶有無(wú)效數據的表單 <html> <head> <title>Collecting your data</title> </head> <body> <form action="http://path.example.com/processStuff.php" method="post"><input type="text" name="answer" value="There is no way this is a valid response to a yes/no answer..." /> <input type="submit" value="Save" name="submit" /> </form> </body> </html>
|
思考一下:如果擁有限制用戶(hù)輸入量的下拉框或單選按鈕,您可能會(huì )認為不用擔心驗證輸入的問(wèn)題。畢竟,輸入表單將確保用戶(hù)只能輸入某些數據,對吧?要限制表單欺騙,需要進(jìn)行驗證以確保發(fā)布者的身份是真實(shí)的。您可以使用一種一次性使用標記,雖然這種技術(shù)仍然不能確保表單絕對安全,但是會(huì )使表單欺騙更加困難。由于在每次調用表單時(shí)都會(huì )更改標記,因此想要成為攻擊者就必須獲得發(fā)送表單的實(shí)例,去掉標記,并把它放到假表單中。使用這項技術(shù)可以阻止惡意用戶(hù)構建持久的 Web 表單來(lái)向應用程序發(fā)布不適當的請求。清單 16提供了一種表單標記示例。
清單 16. 使用一次性表單標記 <?php session_start(); ?> <html> <head> <title>SQL Injection Test</title> </head> <body> <?php
echo 'Session token=' . $_SESSION['token']; echo '<br />'; echo 'Token from form=' . $_POST['token']; echo '<br />';
if ($_SESSION['token'] == $_POST['token']) { /* cool, it's all good... create another one */
} else { echo '<h1>Go away!</h1>'; } $token = md5(uniqid(rand(), true)); $_SESSION['token'] = $token; ?> <form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post"> <div><input type="hidden" name="token" value="<?php echo $token; ?>" /> <input type="text" name="myText" value="<?php echo(isset($_POST['myText']) ? $_POST['myText'] : ''); ?>" /> <input type="submit" value="Save" name="submit" /></div> </form> </body> </html>
|
針對 CSRF 進(jìn)行保護跨站點(diǎn)請求偽造(CSRF 攻擊)是利用用戶(hù)權限執行攻擊的結果。在 CSRF 攻擊中,您的用戶(hù)可以輕易地成為預料不到的幫兇。清單 17 提供了執行特定操作的頁(yè)面示例。此頁(yè)面將從 cookie 中查找用戶(hù)登錄信息。只要 cookie 有效,Web 頁(yè)面就會(huì )處理請求。
清單 17. CSRF 示例 <img src="http://www.example.com/processSomething?id=123456789" />
|
CSRF 攻擊通常是以<img>標記的形式出現的,因為瀏覽器將在不知情的情況下調用該 URL 以獲得圖像。但是,圖像來(lái)源可以是根據傳入參數進(jìn)行處理的同一個(gè)站點(diǎn)中的頁(yè)面 URL。當此<img>標記與 XSS 攻擊結合在一起時(shí) — 在已歸檔的攻擊中最常見(jiàn) — 用戶(hù)可以在不知情的情況下輕松地對其憑證執行一些操作 — 因此是偽造的。
為了保護您免受 CSRF 攻擊,需要使用在檢驗表單 post 時(shí)使用的一次性標記方法。此外,使用顯式的$_POST變量而非$_REQUEST。清單 18 演示了處理相同 Web 頁(yè)面的糟糕示例 — 無(wú)論是通過(guò)GET請求調用頁(yè)面還是通過(guò)把表單發(fā)布到頁(yè)面中。
清單 18. 從$_REQUEST中獲得數據 <html> <head> <title>Processes both posts AND gets</title> </head> <body> <?php if ($_REQUEST['submit'] == 'Save') { echo("<p>I am processing your text: "); echo(htmlentities($_REQUEST['text'])); echo("</p>"); } ?> </body> </html>
|
清單 19 顯示了只使用表單POST的干凈頁(yè)面。
清單 19. 僅從$_POST中獲得數據 <html> <head> <title>Processes both posts AND gets</title> </head> <body> <?php if ($_POST['submit'] == 'Save') { echo("<p>I am processing your text: "); echo(htmlentities($_POST['text'])); echo("</p>"); } ?> </body> </html>
|
結束語(yǔ) 從這七個(gè)習慣開(kāi)始嘗試編寫(xiě)更安全的 PHP Web 應用程序,可以幫助您避免成為惡意攻擊的受害者。和許多其他習慣一樣,這些習慣最開(kāi)始可能很難適應,但是隨著(zhù)時(shí)間的推移遵循這些習慣會(huì )變得越來(lái)越自然。
記住第一個(gè)習慣是關(guān)鍵:驗證輸入。在確保輸入不包括無(wú)效值之后,可以繼續保護文件系統、數據庫和會(huì )話(huà)。最后,確保 PHP 代碼可以抵抗 XSS 攻擊、表單欺騙和 CSRF 攻擊。形成這些習慣后可以幫助您抵御一些簡(jiǎn)單的攻擊。
關(guān)于作者
Nathan Good 居住在明尼蘇達州的雙子城。其專(zhuān)職
工作是軟件開(kāi)發(fā)、軟件架構和系統管理。在不編寫(xiě)軟件時(shí),他喜歡組裝 PC 和服務(wù)器、閱讀和撰寫(xiě)技術(shù)文章,鼓勵他的所有朋友轉用
開(kāi)源軟件。他自己編著(zhù)以及與他人合著(zhù)了很多書(shū)籍和文章,包括 Professional Red Hat Enterprise
Linux 3, Regular Expression Recipes: A Problem-Solution Approach 和 Foundations of PEAR: Rapid PHP Development。