在說起安全性問題時,須要注意,除了實際的平臺和操做系統安全性問題以外,您還須要確保編寫安全的應用程序。在編寫 PHP 應用程序時,請應用下面的七個習慣以確保應用程序具備最好的安全性:javascript
在說起安全性問題時,驗證數據是您可能採用的最重要的習慣。而在說起輸入時,十分簡單:不要相信用戶。您的用戶可能十分優秀,而且大多數用戶可能徹底按照指望來使用應用程序。可是,只要提供了輸入的機會,也就極有可能存在很是糟糕的輸入。做爲一名應用程序開發人員,您必須阻止應用程序接受錯誤的輸入。仔細考慮用戶輸入的位置及正確值將使您能夠構建一個健壯、安全的應用程序。html
雖而後文將介紹文件系統與數據庫交互,可是下面列出了適用於各類驗證的通常驗證提示:java
白名單中的值(White-listed value)是正確的值,與無效的黑名單值(Black-listed value)相對。二者之間的區別是,一般在進行驗證時,可能值的列表或範圍小於無效值的列表或範圍,其中許多值多是未知值或意外值。mysql
在進行驗證時,記住設計並驗證應用程序容許使用的值一般比防止全部未知值更容易。例如,要把字段值限定爲全部數字,須要編寫一個確保輸入全都是數字的例程。不要編寫用於搜索非數字值並在找到非數字值時標記爲無效的例程。正則表達式
2000 年 7 月,一個 Web 站點泄露了保存在 Web 服務器的文件中的客戶數據。該 Web 站點的一個訪問者使用 URL 查看了包含數據的文件。雖然文件被放錯了位置,可是這個例子強調了針對攻擊者保護文件系統的重要性。數據庫
若是 PHP 應用程序對文件進行了任意處理而且含有用戶能夠輸入的變量數據,請仔細檢查用戶輸入以確保用戶沒法對文件系統執行任何不恰當的操做。清單 1 顯示了下載具備指定名的圖像的 PHP 站點示例。瀏覽器
<?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 提供了驗證文件名的示例例程。它將使用正則表達式以確保文件名中僅使用有效字符,而且特別檢查圓點字符:..
。
function isValidFileName($file) { /* don't allow .. and allow any "word" character \ / */ return preg_match('/^(((?:\.)(?!\.))|\w)+$/', $file); } |
2008 年 4 月,美國某個州的獄政局在查詢字符串中使用了 SQL 列名,所以泄露了保密數據。此次泄露容許惡意用戶選擇須要顯示的列、提交頁面並得到數據。此次泄露顯示了用戶如何可以以應用程序開發人員沒法預料的方法執行輸入,並代表了防護 SQL 注入攻擊的必要性。
清單 3 顯示了運行 SQL 語句的示例腳本。在本例中,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 和賬戶編號。
<?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 目錄的內容。
-rw------- 1 _www wheel 97 Aug 18 20:00 sess_9e4233f2cd7cae35866cd8b61d9fa42b |
正如您所見,在輸出時(參見清單 7),會話文件以很是易讀的格式包含信息。因爲該文件必須可由 Web 服務器用戶讀寫,所以會話文件可能爲共享服務器中的全部用戶帶來嚴重的問題。除您以外的某我的能夠編寫腳原本讀取這些文件,所以能夠嘗試從會話中取出值。
userName|s:5:"ngood";accountNumber|s:9:"123456789"; |
您能夠採起兩項操做來保護會話數據。第一是把您放入會話中的全部內容加密。可是正由於加密數據並不意味着絕對安全,所以請慎重採用這種方法做爲保護會話的唯一方式。備選方法是把會話數據存儲在其餘位置中,比方說數據庫。您仍然必須確保鎖定數據庫,可是這種方法將解決兩個問題:第一,它將把數據放到比共享文件系統更加安全的位置;第二,它將使您的應用程序能夠更輕鬆地跨越多個 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 漏洞表明 2007 年全部歸檔的 Web 站點的大部分漏洞(請參閱 參考資料)。當用戶可以把 HTML 代碼注入到您的 Web 頁面中時,就是出現了 XSS 漏洞。HTML 代碼能夠在腳本標記中攜帶 JavaScript 代碼,於是只要提取頁面就容許運行 JavaScript。清單 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 攻擊的表單如何輸出結果。
<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)目的(請參閱 參考資料)。
<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 應用程序的名稱、電子郵件地址、電話號碼和賬單信息的輸入中用白名單中的值驗證輸入數據。
下面顯示了更安全的顯示文本輸入的頁面。
<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 從某個不恰當的位置發到您的表單中。欺騙表單的最簡單方法就是建立一個經過提交至表單來傳遞全部值的 Web 頁面。因爲 Web 應用程序是沒有狀態的,所以沒有一種絕對可行的方法能夠確保所發佈數據來自指定位置。從 IP 地址到主機名,全部內容都是能夠欺騙的。清單 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 文檔。在保存表單後,在瀏覽器中打開該表單。而後能夠填寫數據並提交表單,從而觀察如何處理數據。
<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 中的代碼,其中包含帶有無效數據的表單。
<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 提供了一種表單標記示例。
<?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 攻擊中,您的用戶能夠輕易地成爲預料不到的幫兇。清單 17 提供了執行特定操做的頁面示例。此頁面將從 cookie 中查找用戶登陸信息。只要 cookie 有效,Web 頁面就會處理請求。
<img src="http://www.example.com/processSomething?id=123456789" /> |
CSRF 攻擊一般是以 <img>
標記的形式出現的,由於瀏覽器將在不知情的狀況下調用該 URL 以得到圖像。可是,圖像來源能夠是根據傳入參數進行處理的同一個站點中的頁面 URL。當此 <img>
標記與 XSS 攻擊結合在一塊兒時 — 在已歸檔的攻擊中最多見 — 用戶能夠在不知情的狀況下輕鬆地對其憑證執行一些操做 — 所以是僞造的。
爲了保護您免受 CSRF 攻擊,須要使用在檢驗表單 post 時使用的一次性標記方法。此外,使用顯式的 $_POST
變量而非 $_REQUEST
。清單 18 演示了處理相同 Web 頁面的糟糕示例 — 不管是經過 GET
請求調用頁面仍是經過把表單發佈到頁面中。
<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
的乾淨頁面。
<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 攻擊。造成這些習慣後能夠幫助您抵禦一些簡單的攻擊。