安全|問題
有時候,您的業務可能涉及到 PHP 應用程式的安全性。當您遇到審計任務時,您知道如何執行尋找嗎?本系列將帶您進入 PHP,並幫您在一定程式上瞭解它,讓您在進行安全審計時知道尋找什麼。第 1 部分向您介紹 register_globals 設定。
入門知識
我在此假定您對 PHP 的文法有一個大致的瞭解,至少能夠編寫“Hello World”之類的程式。如果您不具備基礎知識,則請首先學習 PHP 手冊和某些基本的 PHP 教程(參閱 參考資料)。很多出版商都有關於 PHP 的好書。建議初學者一開始先看看入門書籍或食譜形式的書籍。
在生產環境的準確副本上執行審計。您不需要複製硬體,但是需要確保軟體版本盡量和實際的完全一樣。PHP 配置必須精確匹配,這一點在 php.ini 檔案中、在 .htaccess 檔案的 Apache 指令中或在 httpd.conf 中已經指定。您需要準備一個單獨的環境,因為您將顯示和記錄可能包含敏感的密碼及其他資訊的錯誤。此外,您將嘗試中斷網站的安全性,這一點是您在活動應用程式中極力避免的。
第一步是將 PHP 的 error_reporting 設定更改為 E_ALL。設定更改後,每當使用未初始化的變數、進行錯誤的檔案訪問及發生其他(大多數)無害錯誤時,PHP 都會報告一條警告訊息,但也存在這是一個潛在攻擊向量的可能性。這些錯誤一般情況下只是表明編程草率,所以如果這是您的代碼,您把它們清除掉即可。
該設定如下所示:
error_reporting = E_ALL
如果您不知道 php.ini 檔案在哪裡,則可以通過建立包含以下文本的 .php 指令碼來尋找:
<?php
phpinfo();
輸出的上面部分有一行列出了 PHP 尋找 php.ini 的位置:
圖 1. PHP 尋找 php.ini 的位置
[[The No.1 Picture.]]
值可能會有些變化,但 /usr/local/lib/php.ini 是大多數 UNIX? 系統上的公用位置,C:\php\php.ini 或 C:\WINDOWS\php.ini 是大多數 Microsoft? Windows? 系統上的公用位置。如果該檔案不存在,則建立一個並在檔案中鍵入上面的 error_reporting 行即可。修改 php.ini 檔案後,需要重啟 Web 服務器,PHP 才能啟用新設定。
如果您以前沒有建立 phpinfo() 頁面,則可以現在建立。第二個主要部分的標籤是“配置”,它包含許多關於如何設定 PHP 的有用資訊。該部分包括三列:設定名稱、本地值 和 xmaster 值。主值是通過 php.ini 指令為您機器上的所有 PHP 指令碼全域設定的值。本地值是對當前指令碼生效的值。對它有影響的有:.htaccess 設定、httpd.conf 的 <Location> 或 <Directory> 部分中的設定和 PHP 指令碼中的 ini_set 調用。在運行時,只有某些設定是可更改的。請參閱 參考資料中的 PHP 手冊以擷取詳細資料。
還需要自訂的另外兩種設定是 display_errors 和 log_errors。您至少需要啟用這兩種設定中的一種,或者兩種都啟用。log_errors 通知 PHP 將注意、警告或錯誤記錄在檔案中,display_errors 將這些被記錄下來的注意、警告和錯誤顯示在螢幕上。它們不互斥。至少啟用它們中的一個,可以有效地發現可能導致安全性漏洞的編程錯誤。
應該尋找哪些種類的安全問題?
值得慶幸的是,導致安全性漏洞的很多編程錯誤在 PHP 中不可能存在。堆棧和緩衝溢出是 C 和 C++ 環境中兩個常見的問題。因為 PHP 可以為您管理記憶,所以 PHP 代碼不會導致堆棧和緩衝溢出。
然而,PHP 本身也是使用 C 語言編寫的,有時記憶問題深至 PHP 的核面。因此,您需要時時關注安全公報和更新。PHP 在其 Web 網站(參見 參考資料)公布新 PHP 版本並說明是否包含安全Hotfix。
PHP 應用程式中的大多數問題與使用使用者提供的資料有關,在使用它和對它執行操作前未曾預先驗證和消毒。您可能聽說過稱為 cross-site scripting (XSS) 的漏洞。XSS 通過提供者不期望的輸入,然後利用程式對無賴輸入的處理方式發動進攻。編寫良好的程式可以避免這些假定。在機場安全方面,PHP 程式用於檢查旅客的行李。
其他問題是一些細微的邏輯錯誤。例如,檢查一系列參數,看看是否批准某個使用者訪問某種資源、是否把括弧放錯位置以至於某些使用者進入了他們原本不該到的地方。我們希望您的應用程式組織良好並具有這種集中式邏輯。
識別使用者輸入
最棘手的一件事情是如何從外部源(如某個使用者、別的 Web 網站或某些其他資源)和已經驗證的資料中區分出不受信任的輸入。有人提出了“不相信一切”的觀點,即不管來自何處,對於所有函數都要驗證其資料。這一做法會牽涉到以下幾件事情:第一,驗證在不同的上下文中意味著不同的事情;第二,在應用程式的所有層級上快速執行驗證是一件枯燥乏味和易於出錯的事情;第三,您是在審計應用程式而不是在從頭重新編寫它。您需要通過現有代碼來跟蹤使用者輸入,而不能用驗證函式封裝您看到的每個變數。
不期望的使用者輸入
使用者輸入從何而來?第一個源是 GET、POST 和 COOKIE 資料。一般稱為 GPC 資料。此資料的可識別程式依賴於一個有爭議的 php.ini 設定:register_globals。在 PHP V4.3.0 以後,register_globals 預設情況下被設定為 Off。但是幾年前,在 PHP 中,register_globals 的預設值是開啟的,所以存在很多需要它的代碼。
register_globals 本身並非安全風險。但是,它為跟蹤使用者輸入和確保應用程式安全增加了難度。為什麼會這樣?因為如果開啟 register_globals,在全域名稱空間和 $_GET、$_POST 或 $_COOKIE 數組中,將建立 GET、POST 和 COOKIE 傳遞到 PHP 指令碼的所有變數。
下面是工作方式及其重要性的樣本:
清單 1. COOKIE 的安全性
1 <?php
2
3 // See if the user has the secret cookie.
4 if (!empty($_COOKIE['secret'])) {
5 $authorized = true;
6 }
7
8 // Now let's go through a list of press releases and show them.
9 $releases = get_press_releases();
10 foreach ($releases as $release) {
11
12 // Some releases are restricted. Only show them to people who can
13 // see secrets.
14 if ($release['secret']) {
15 if (!$authorized) {
16 continue;
17 }
18 }
19
20 // We must be allowed to see it.
21 showRelease($release);
22 }
您應該注意幾件事。第一,依靠 cookie 來判斷使用者是否已通過身分識別驗證不是個好主意 —— 因為人們可以很容易地設定自己的 cookie 值。我們將在另外一篇文章中敘述這一點。無論如何,此指令碼的缺點在於,如果開啟 register_globals,它就不具備安全性了。
下面介紹名為 press.php 的指令碼。一般來說,當使用者訪問 press 發行版的指令碼時,其瀏覽器將顯示 http://www.example.com/company/press.php。
現在注意當使用者擅自將其更改為 http://www.example.com/company/press.php?authorized=1 時將發生什麼事?
看看前面的代碼:僅當使用者使用 cookie 時才設定 $authorized。它永遠不會被設定為假。後來引入了 register_globals —— 它取代了剛才使用的 $_GET['authorized'],同時在全域範圍內還存在一個值為 1 的變數 $authorized。因此,即使使用者沒有通過 cookie 檢查,$authorized 後來在 foreach 迴圈中引用時,仍然會被驗證為真。
修複此缺陷可以使用兩種方式。其一,當然是關閉 register_globals。如果關閉它對您的生產網站沒有影響,則這是個好主意。您需要測試一下應用程式,確保它沒有因此中斷運行。
另一種方式有點像“防禦性編程”。我們只需要將 cookie 檢查更改為以下形式即可:
清單 2. 使用 COOKIE 提高安全性
1 <?php
2
3 // See if the user has the secret cookie.
4 $authorized = false;
5 if (!empty($_COOKIE['secret'])) {
6 $authorized = true;
7 }
...
這時,當使用者將 ?authorized=1 添加到指令碼 URL 時,$authorized 變數仍然被設定為 1 —— 但是它隨即會被 $authorized = false 覆蓋,只有那些實際具有秘密 cookie 的使用者才能看到受限的 press 發行版。他們仍然可以設計自己的 cookie。
審計代碼的教訓:設法關閉 register_globals。如果不開啟 register_globals 應用程式就不能運行,並且您無法修改它,或者在應用程式必須啟動並執行地方您無法控制 PHP 配置,則需要在條件塊中尋找所有全域變數設定,或者通過某些函數調用進入全域範圍。如果 register_globals 為開啟狀態,則這兩種情形都是由使用者將變數設定為任意值引起的。
找到這些變數的好辦法是將 php.ini 設定 error_reporting 設定為 E_ALL,同時使用 log_errors 或 display_errors,這樣,所有 PHP 警告和錯誤都會被分別記錄在檔案中或顯示在螢幕上。每當使用未初始化的變數(假定具有值)時,您將得到一條 E_NOTICE。這像 C 和 Java? 語言中那樣,仍然與讓 PHP 要求聲明 變數有所不同。結果,當我們的第一個版本的指令碼運行時,出現的錯誤訊息是:
Notice: Undefined variable: authorized in C:\var\www\articles\press.php
on line 15