在提及安全性問題時,需要注意,除了實際的平台和作業系統安全性問題之外,您還需要確定編寫安全的應用程式。在編寫 PHP 應用程式時,請應用下面的七個習慣以確定應用程式具有最好的安全性:
- 驗證輸入
- 保護檔案系統
- 保護資料庫
- 保護會話資料
- 保護跨站點腳本(Cross-site scripting,XSS)漏洞
- 檢驗表單 post
- 針對跨站點請求僞造(Cross-Site Request Forgeries,CSRF)進行保護
在提及安全性問題時,驗證資料是您可能采用的最重要的習慣。而在提及輸入時,十分簡單:不要相信使用者。您的使用者可能十分優秀,并且大多數使用者可能完全按照期望來使用應用程式。但是,隻要提供了輸入的機會,也就極有可能存在非常糟糕的輸入。作為一名應用程式開發人員,您必須阻止應用程式接受錯誤的輸入。仔細考慮使用者輸入的位置及正确值将使您可以建構一個健壯、安全的應用程式。
雖然後文将介紹檔案系統與資料庫互動,但是下面列出了适用于各種驗證的一般驗證提示:
- 使用白名單中的值
- 始終重新驗證有限的選項
- 使用内置轉義函數
- 驗證正确的資料類型(如數字)
白名單中的值(White-listed value)是正确的值,與無效的黑名單值(Black-listed value)相對。兩者之間的差別是,通常在進行驗證時,可能值的清單或範圍小于無效值的清單或範圍,其中許多值可能是未知值或意外值。
在進行驗證時,記住設計并驗證應用程式允許使用的值通常比防止所有未知值更容易。例如,要把字段值限定為所有數字,需要編寫一個確定輸入全都是數字的例程。不要編寫用于搜尋非數字值并在找到非數字值時标記為無效的例程。
2000 年 7 月,一個 Web 站點洩露了儲存在 Web 伺服器的檔案中的客戶資料。該 Web 站點的一個通路者使用 URL 檢視了包含資料的檔案。雖然檔案被放錯了位置,但是這個例子強調了針對攻擊者保護檔案系統的重要性。
如果 PHP 應用程式對檔案進行了任意處理并且含有使用者可以輸入的變量資料,請仔細檢查使用者輸入以確定使用者無法對檔案系統執行任何不恰當的操作。清單 1 顯示了下載下傳具有指定名的圖像的 PHP 站點示例。
清單 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>");
}
|
正如您所見,清單 1 中比較危險的腳本将處理 Web 伺服器擁有讀取權限的所有檔案,包括會話目錄中的檔案(請參閱 “
”),甚至還包括一些系統檔案(例如
/etc/passwd
)。為了進行示範,這個示例使用了一個可供使用者鍵入檔案名的文本框,但是可以在查詢字元串中輕松地提供檔案名。
同時配置使用者輸入和檔案系統通路權十分危險,是以最好把應用程式設計為使用資料庫和隐藏生成的檔案名來避免同時配置。但是,這樣做并不總是有效。清單 2 提供了驗證檔案名的示例例程。它将使用正規表達式以確定檔案名中僅使用有效字元,并且特别檢查圓點字元:
..
。
清單 2. 檢查有效的檔案名字元
function isValidFileName($file) {
/* don't allow .. and allow any "word" character \ / */
return preg_match('/^(((?:\.)(?!\.))|\w)+$/', $file);
}
|
回頁首 2008 年 4 月,美國某個州的獄政局在查詢字元串中使用了 SQL 列名,是以洩露了保密資料。這次洩露允許惡意使用者選擇需要顯示的列、送出頁面并獲得資料。這次洩露顯示了使用者如何能夠以應用程式開發人員無法預料的方法執行輸入,并表明了防禦 SQL 注入攻擊的必要性。
清單 3 顯示了運作 SQL 語句的示例腳本。在本例中,SQL 語句是允許相同攻擊的動态語句。此表單的所有者可能認為表單是安全的,因為他們已經把列名限定為選擇清單。但是,代碼疏忽了關于表單欺騙的最後一個習慣 — 代碼将選項限定為下拉框并不意味着其他人不能夠釋出含有所需内容的表單(包括星号 [
*
])。
清單 3. 執行 SQL 語句
<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>
|
是以,要形成保護資料庫的習慣,請盡可能避免使用動态 SQL 代碼。如果無法避免動态 SQL 代碼,請不要對列直接使用輸入。清單 4 顯示了除使用靜态列外,還可以向帳戶編号字段添加簡單驗證例程以確定輸入值不是非數字值。
清單 4. 通過驗證和
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()
函數的用法。此函數将正确地過濾您的輸入,是以它不包括無效字元。如果您一直依賴于
magic_quotes_gpc
,那麼需要注意它已被棄用并且将在 PHP V6 中删除。從現在開始應避免使用它并在此情況下編寫安全的 PHP 應用程式。此外,如果使用的是 ISP,則有可能您的 ISP 沒有啟用
magic_quotes_gpc
最後,在改進的示例中,您可以看到該 SQL 語句和輸出沒有包括動态列選項。使用這種方法,如果把列添加到稍後含有不同資訊的表中,則可以輸出這些列。如果要使用架構以與資料庫結合使用,則您的架構可能已經為您執行了 SQL 驗證。確定查閱文檔以保證架構的安全性;如果仍然不确定,請進行驗證以確定穩妥。即使使用架構進行資料庫互動,仍然需要執行其他驗證。
保護會話
預設情況下,PHP 中的會話資訊将被寫入臨時目錄。考慮清單 5 中的表單,該表單将顯示如何存儲會話中的使用者 ID 和帳戶編号。
清單 5. 存儲會話中的資料
<?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 目錄中的會話檔案
-rw------- 1 _www wheel 97 Aug 18 20:00 sess_9e4233f2cd7cae35866cd8b61d9fa42b
|
正如您所見,在輸出時(參見清單 7),會話檔案以非常易讀的格式包含資訊。由于該檔案必須可由 Web 伺服器使用者讀寫,是以會話檔案可能為共享伺服器中的所有使用者帶來嚴重的問題。除您之外的某個人可以編寫腳本來讀取這些檔案,是以可以嘗試從會話中取出值。
清單 7. 會話檔案的内容
userName|s:5:"ngood";accountNumber|s:9:"123456789";
|
存儲密碼
不管是在資料庫、會話、檔案系統中,還是在任何其他表單中,無論如何密碼都決不能存儲為純文字。處理密碼的最佳方法是将其加密存儲并互相比較加密的密碼。雖然如此,在實踐中人們仍然把密碼存儲到純文字中。隻要使用可以發送密碼而非重置密碼的 Web 站點,就意味着密碼是存儲在純文字中或者可以獲得用于解密的代碼(如果加密的話)。即使是後者,也可以找到并使用解密代碼。
您可以采取兩項操作來保護會話資料。第一是把您放入會話中的所有内容加密。但是正因為加密資料并不意味着絕對安全,是以請慎重采用這種方法作為保護會話的惟一方式。備選方法是把會話資料存儲在其他位置中,比方說資料庫。您仍然必須確定鎖定資料庫,但是這種方法将解決兩個問題:第一,它将把資料放到比共享檔案系統更加安全的位置;第二,它将使您的應用程式可以更輕松地跨越多個 Web 伺服器,同時共享會話可以跨越多個主機。
要實作自己的會話持久性,請參閱 PHP 中的
session_set_save_handler()
函數。使用它,您可以将會話資訊存儲在資料庫中,也可以實作一個用于加密和解密所有資料的處理程式。清單 8 提供了實作的函數用法和函數骨架示例。您還可以在
參考資料 小節中檢視如何使用資料庫。
清單 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 漏洞進行保護
XSS 漏洞代表 2007 年所有歸檔的 Web 站點的大部分漏洞(請參閱
)。當使用者能夠把 HTML 代碼注入到您的 Web 頁面中時,就是出現了 XSS 漏洞。HTML 代碼可以在腳本标記中攜帶 JavaScript 代碼,因而隻要提取頁面就允許運作 JavaScript。清單 9 中的表單可以表示論壇、維基、社會網絡或任何可以輸入文本的其他站點。
清單 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 提供了一個基本示例,在該示例中将彈出一個新視窗并打開 Google 的首頁。如果您的 Web 應用程式不針對 XSS 攻擊進行保護,則會造成嚴重的破壞。例如,某個人可以添加模仿站點樣式的連結以達到欺騙(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 攻擊,隻要變量的值将被列印到輸出中,就需要通過
htmlentities()
函數過濾輸入。記住要遵循第一個習慣:在 Web 應用程式的名稱、電子郵件位址、電話号碼和帳單資訊的輸入中用白名單中的值驗證輸入資料。
下面顯示了更安全的顯示文本輸入的頁面。
清單 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>
|
針對無效 post 進行保護
表單欺騙 是指有人把 post 從某個不恰當的位置發到您的表單中。欺騙表單的最簡單方法就是建立一個通過送出至表單來傳遞所有值的 Web 頁面。由于 Web 應用程式是沒有狀态的,是以沒有一種絕對可行的方法可以確定所釋出資料來自指定位置。從 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 顯示了将釋出到清單 13 所示表單中的表單。要嘗試此操作,您可以把該表單放到 Web 站點中,然後把清單 14 中的代碼另存為桌面上的 HTML 文檔。在儲存表單後,在浏覽器中打開該表單。然後可以填寫資料并送出表單,進而觀察如何處理資料。
清單 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>
|
表單欺騙的潛在影響是,如果擁有含下拉框、單選按鈕、複選框或其他限制輸入的表單,則當表單被欺騙時這些限制沒有任何意義。考慮清單 15 中的代碼,其中包含帶有無效資料的表單。
清單 15. 帶有無效資料的表單
<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>
|
思考一下:如果擁有限制使用者輸入量的下拉框或單選按鈕,您可能會認為不用擔心驗證輸入的問題。畢竟,輸入表單将確定使用者隻能輸入某些資料,對吧?要限制表單欺騙,需要進行驗證以確定釋出者的身份是真實的。您可以使用一種一次性使用标記,雖然這種技術仍然不能確定表單絕對安全,但是會使表單欺騙更加困難。由于在每次調用表單時都會更改标記,是以想要成為攻擊者就必須獲得發送表單的執行個體,去掉标記,并把它放到假表單中。使用這項技術可以阻止惡意使用者建構持久的 Web 表單來向應用程式釋出不适當的請求。清單 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 進行保護
跨站點請求僞造(CSRF 攻擊)是利用使用者權限執行攻擊的結果。在 CSRF 攻擊中,您的使用者可以輕易地成為預料不到的幫兇。清單 17 提供了執行特定操作的頁面示例。此頁面将從 cookie 中查找使用者登入資訊。隻要 cookie 有效,Web 頁面就會處理請求。
清單 17. CSRF 示例
<img src="http://www.example.com/processSomething?id=123456789" />
|
CSRF 攻擊通常是以
<img>
标記的形式出現的,因為浏覽器将在不知情的情況下調用該 URL 以獲得圖像。但是,圖像來源可以是根據傳入參數進行處理的同一個站點中的頁面 URL。當此
<img>
标記與 XSS 攻擊結合在一起時 — 在已歸檔的攻擊中最常見 — 使用者可以在不知情的情況下輕松地對其憑證執行一些操作 — 是以是僞造的。
為了保護您免受 CSRF 攻擊,需要使用在檢驗表單 post 時使用的一次性标記方法。此外,使用顯式的
$_POST
變量而非
$_REQUEST
。清單 18 示範了處理相同 Web 頁面的糟糕示例 — 無論是通過
GET
請求調用頁面還是通過把表單釋出到頁面中。
清單 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
的幹淨頁面。
清單 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>
|
結束語
從這七個習慣開始嘗試編寫更安全的 PHP Web 應用程式,可以幫助您避免成為惡意攻擊的受害者。和許多其他習慣一樣,這些習慣最開始可能很難适應,但是随着時間的推移遵循這些習慣會變得越來越自然。
記住第一個習慣是關鍵:驗證輸入。在確定輸入不包括無效值之後,可以繼續保護檔案系統、資料庫和會話。最後,確定 PHP 代碼可以抵抗 XSS 攻擊、表單欺騙和 CSRF 攻擊。形成這些習慣後可以幫助您抵禦一些簡單的攻擊。