php 應用程式中的安全性包括遠程安全性和本地安全性。本文將揭示 PHP 開發人員在實現具有這兩種安全性的 Web 應用程式時應該養成的習慣。
在提及安全性問題時,需要注意,除了實際的平台和作業系統安全性問題之外,您還需要確保編寫安全的應用程式。在編寫 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. 下載檔案
<");echo("title>Guard your filesystem");echo("");}
正如您所見,清單 1 中比較危險的指令碼將處理 Web 服務器擁有讀取許可權的所有檔案,包括會話目錄中的檔案(請參閱 “保護會話資料”),甚至還包括一些系統檔案(例如/etc/passwd)。為了進行示範,這個樣本使用了一個可供使用者鍵入檔案名稱的文字框,但是可以在查詢字串中輕鬆地提供檔案名稱。
同時配置使用者輸入和檔案系統訪問權十分危險,因此最好把應用程式設計為使用資料庫和隱藏產生的檔案名稱來避免同時配置。但是,這樣做並不總是有效。清單 2 提供了驗證檔案名稱的樣本常式。它將使用Regex以確保檔案名稱中僅使用有效字元,並且特別檢查圓點字元:..。
清單 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 陳述式
SQL Injection Example' . $select . '
';$result = mysql_query($select) or die('
' . mysql_error() . '
');echo '
';while ($row = mysql_fetch_assoc($result)) {echo '
';echo '
' . $row[$col] . ' | ';echo '
';}echo '
';mysql_close($link);}?>
因此,要形成保護資料庫的習慣,請儘可能避免使用動態 SQL 代碼。如果無法避免動態 SQL 代碼,請不要對列直接使用輸入。清單 4 顯示了除使用靜態列外,還可以向帳戶編號欄位添加簡單驗證常式以確保輸入值不是非數字值。
清單 4. 通過驗證和mysql_real_escape_string()提供保護
SQL Injection Example' . $select . '
';$result = mysql_query($select) or die('
' . mysql_error() . '
');echo '
';while ($row = mysql_fetch_assoc($result)) {echo '
';echo '
' . $row['account_number'] . ' | ';echo '
' . $row['name'] . ' | ';echo '
' . $row['address'] . ' | ';echo '
';}echo '
';mysql_close($link);} else {echo "" ."Please supply a valid account number!";}}?>
本例還展示了mysql_real_escape_string()函數的用法。此函數將正確地過濾您的輸入,因此它不包括無效字元。如果您一直依賴於magic_quotes_gpc,那麼需要注意它已被棄用並且將在 PHP V6 中刪除。從現在開始應避免使用它並在此情況下編寫安全的 PHP 應用程式。此外,如果使用的是 ISP,則有可能您的 ISP 沒有啟用magic_quotes_gpc。
最後,在改進的樣本中,您可以看到該 SQL 陳述式和輸出沒有包括動態列選項。使用這種方法,如果把列添加到稍後含有不同資訊的表中,則可以輸出這些列。如果要使用架構以與資料庫結合使用,則您的架構可能已經為您執行了 SQL 驗證。確保查閱文檔以保證架構的安全性;如果仍然不確定,請進行驗證以確保穩妥。即使使用架構進行資料庫互動,仍然需要執行其他驗證。
保護會話
預設情況下,PHP 中的會話資訊將被寫入臨時目錄。考慮清單 5 中的表單,該表單將顯示如何儲存會話中的使用者識別碼 和帳戶編號。
清單 5. 儲存會話中的資料
Storing session information
清單 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. 輸入文本的表單
Your chance to input XSS
清單 10 示範了允許 XSS 攻擊的表單如何輸出結果。
清單 10. showResults.php
Results demonstrating XSSYou typed this:
");echo("
");echo($_POST['myText']);echo("
");?>
清單 11 提供了一個基本樣本,在該樣本中將彈出一個新視窗並開啟 Google 的首頁。如果您的 Web 應用程式不針對 XSS 攻擊進行保護,則會造成嚴重的破壞。例如,某個人可以添加模仿網站樣式的連結以達到欺騙(phishing)目的(請參閱參考資料)。
清單 11. 惡意輸入文本範例
要防止受到 XSS 攻擊,只要變數的值將被列印到輸出中,就需要通過htmlentities()函數過濾輸入。記住要遵循第一個習慣:在 Web 應用程式的名稱、電子郵件地址、電話號碼和帳單資訊的輸入中用白名單中的值驗證輸入資料。
下面顯示了更安全的顯示文本輸入的頁面。
清單 12. 更安全的表單
Results demonstrating XSSYou typed this:
");echo("
");echo(htmlentities($_POST['myText']));echo("
");?>
針對無效 post 進行保護
表單欺騙是指有人把 post 從某個不恰當的位置發到您的表單中。欺騙表單的最簡單方法就是建立一個通過提交至表單來傳遞所有值的 Web 頁面。由於 Web 應用程式是沒有狀態的,因此沒有一種絕對可行的方法可以確保所發布資料來自指定位置。從 IP 位址到主機名稱,所有內容都是可以欺騙的。清單 13 顯示了允許輸入資訊的典型表單。
清單 13. 處理文本的表單
Form spoofing exampleI am processing your text: ");echo($_POST['myText']);echo("
");}?>
清單 14 顯示了將發布到清單 13 所示表單中的表單。要嘗試此操作,您可以把該表單放到 Web 網站中,然後把清單 14 中的代碼另存新檔案頭上的 HTML 文檔。在儲存表單後,在瀏覽器中開啟該表單。然後可以填寫資料並提交表單,從而觀察如何處理資料。
清單 14. 收集資料的表單
Collecting your data
表單欺騙的潛在影響是,如果擁有含下拉框、選項按鈕、複選框或其他限制輸入的表單,則當表單被欺騙時這些限制沒有任何意義。考慮清單 15 中的代碼,其中包含帶有無效資料的表單。
清單 15. 帶有無效資料的表單
Collecting your data
思考一下:如果擁有限制使用者輸入量的下拉框或選項按鈕,您可能會認為不用擔心驗證輸入的問題。畢竟,輸入表單將確保使用者只能輸入某些資料,對吧?要限制表單欺騙,需要進行驗證以確保發行者的身份是真實的。您可以使用一種一次性使用標記,雖然這種技術仍然不能確保表單絕對安全,但是會使表單欺騙更加困難。由於在每次調用表單時都會變更標記,因此想要成為攻擊者就必須獲得發送表單的執行個體,去掉標記,並把它放到假表單中。使用這項技術可以阻止惡意使用者構建持久的 Web 表單來嚮應用程式發布不適當的請求。清單 16 提供了一種表單標記樣本。
清單 16. 使用一次性表單標記
SQL Injection Test';echo 'Token from form=' . $_POST['token'];echo '
';if ($_SESSION['token'] == $_POST['token']) {/* cool, it's all good... create another one */} else {echo '
Go away!
';}$token = md5(uniqid(rand(), true));$_SESSION['token'] = $token;?>
針對 CSRF 進行保護
跨網站請求偽造(CSRF 攻擊)是利用使用者權限執行攻擊的結果。在 CSRF 攻擊中,您的使用者可以輕易地成為預料不到的幫凶。清單 17 提供了執行特定操作的頁面樣本。此頁面將從 cookie 中尋找使用者登入資訊。只要 cookie 有效,Web 頁面就會處理請求。
清單 17. CSRF 樣本
CSRF 攻擊通常是以標記的形式出現的,因為瀏覽器將在不知情的情況下調用該 URL 以獲得映像。但是,映像來源可以是根據傳入參數進行處理的同一個網站中的頁面 URL。當此標記與 XSS 攻擊結合在一起時 — 在已歸檔的攻擊中最常見 — 使用者可以在不知情的情況下輕鬆地對其憑證執行一些操作 — 因此是偽造的。
為了保護您免受 CSRF 攻擊,需要使用在檢驗表單 post 時使用的一次性標記方法。此外,使用顯式的$_POST變數而非$_REQUEST。清單 18 示範了處理相同 Web 頁面的糟糕樣本 — 無論是通過GET請求調用頁面還是通過把表單發布到頁面中。
清單 18. 從$_REQUEST中獲得資料
Processes both posts AND getsI am processing your text: ");echo(htmlentities($_REQUEST['text']));echo("
");}?>
清單 19 顯示了只使用表單POST的乾淨頁面。
清單 19. 僅從$_POST中獲得資料
Processes both posts AND getsI am processing your text: ");echo(htmlentities($_POST['text']));echo("
");}?>
結束語
從這七個習慣開始嘗試編寫更安全的 PHP Web 應用程式,可以協助您避免成為惡意攻擊的受害者。和許多其他習慣一樣,這些習慣最開始可能很難適應,但是隨著時間的推移遵循這些習慣會變得越來越自然。
記住第一個習慣是關鍵:驗證輸入。在確保輸入不包括無效值之後,可以繼續保護檔案系統、資料庫和會話。最後,確保 PHP 代碼可以抵抗 XSS 攻擊、表單欺騙和 CSRF 攻擊。形成這些習慣後可以協助您抵禦一些簡單的攻擊。
關於作者
Nathan Good 居住在明尼蘇達州的雙子城。其專職工作是軟體開發、軟體架構和系統管理。在不編寫軟體時,他喜歡組裝 PC 和伺服器、閱讀和撰寫技術文章,鼓勵他的所有朋友轉用開源軟體。他自己編著以及與他人合著了很多書籍和文章,包括 Professional Red Hat Enterprise linux 3, Regular Expression Recipes: A Problem-Solution Approach 和 Foundations of PEAR: Rapid PHP Development。