談談關於PHP的程式碼安全相關的一些致命知識

edithfang發表於2014-09-15
目標

本教程講解如何防禦最常見的安全威脅:SQL 注入、操縱 GET 和 POST變數、緩衝區溢位攻擊、跨站點指令碼攻擊、瀏覽器內的資料操縱和遠端表單提交。

前提條件

本教程是為至少有一年程式設計經驗的 PHP開發人員編寫的。您應該瞭解 PHP 的語法和約定;這裡不解釋這些內容。有使用其他語言(比如 Ruby、Python 和Perl)的經驗的開發人員也能夠從本教程中受益,因為這裡討論的許多規則也適用於其他語言和環境。

安全性快速簡介

Web應用程式最重要的部分是什麼?根據回答問題的人不同,對這個問題的答案可能是五花八門。業務人員需要可靠性和可伸縮性。IT支援團隊需要健壯的可維護的程式碼。終端使用者需要漂亮的使用者介面和執行任務時的高效能。但是,如果回答 “安全性”,那麼每個人都會同意這對 Web應用程式很重要。

但是,大多數討論到此就打住了。儘管安全性在專案的檢查表中,但是往往到了專案交付之前才開始考慮解決安全性問題。採用這種方式的 Web應用程式專案的數量多得驚人。開發人員工作幾個月,只在最後才新增安全特性,從而讓 Web應用程式能夠向公眾開放。

結果往往是一片混亂,甚至需要返工,因為程式碼已經經過檢驗、單元測試並整合為更大的框架,之後才在其中新增安全特性。新增安全性之後,主要元件可能會停止工作。安全性的整合使得原本順暢(但不安全)的過程增加額外負擔或步驟。
本教程提供一種將安全性整合到PHP Web應用程式中的好方法。它討論幾個一般性安全主題,然後深入討論主要的安全漏洞以及如何堵住它們。在學完本教程之後,您會對安全性有更好的理解。

主題包括:
  • SQL隱碼攻擊
  • 操縱 GET 字串
  • 緩衝區溢位攻擊
  • 跨站點指令碼攻擊(XSS)
  • 瀏覽器內的資料操縱
  • 遠端表單提交
  • Web安全性 101
在討論實現安全性的細節之前,最好從比較高的角度討論 Web 應用程式安全性。本節介紹安全哲學的一些基本信條,無論正在建立何種Web 應用程式,都應該牢記這些信條。這些思想的一部分來自 Chris Shiflett(他關於 PHP 安全性的書是無價的寶庫),一些來自 SimsonGarfinkel(參見 參考資料),還有一些來自多年積累的知識。

規則 1:絕不要信任外部資料或輸入

關於 Web應用程式安全性,必須認識到的第一件事是不應該信任外部資料。外部資料(outside data) 包括不是由程式設計師在 PHP程式碼中直接輸入的任何資料。在採取措施確保安全之前,來自任何其他來源(比如 GET 變數、表單 POST、資料庫、配置檔案、會話變數或cookie)的任何資料都是不可信任的。

例如,下面的資料元素可以被認為是安全的,因為它們是在 PHP 中設定的。

清單 1.安全無暇的程式碼
[php]$myUsername = ‘tmyer’;
$arrayUsers = 
array(’tmyer’, ‘tom’, ‘tommy’);
define(”GREETING”, ‘hello there’ . 
$myUsername); [/php]

但是,下面的資料元素都是有瑕疵的。

清單 2. 不安全、有瑕疵的程式碼
[php]$myUsername = $_POST['username']; //tainted!
$arrayUsers = 
array($myUsername, ‘tom’, ‘tommy’); //tainted!
define(”GREETING”, ‘hello 
there’ . $myUsername); //tainted! [/php]

為什麼第一個變數 $myUsername 是有瑕疵的?因為它直接來自表單POST。使用者可以在這個輸入域中輸入任何字串,包括用來清除檔案或執行以前上傳的檔案的惡意命令。您可能會問,“難道不能使用只接受字母 A-Z的客戶端(JavaScript)表單檢驗指令碼來避免這種危險嗎?”是的,這總是一個有好處的步驟,但是正如在後面會看到的,任何人都可以將任何表單下載到自己的機器上,修改它,然後重新提交他們需要的任何內容。

解決方案很簡單:必須對$_POST['username'] 執行清理程式碼。如果不這麼做,那麼在使用 $myUsername的任何其他時候(比如在陣列或常量中),就可能汙染這些物件。

對使用者輸入進行清理的一個簡單方法是,使用正規表示式來處理它。在這個示例中,只希望接受字母。將字串限制為特定數量的字元,或者要求所有字母都是小寫的,這可能也是個好主意。

清單3. 使使用者輸入變得安全
[php]$myUsername = 
cleanInput($_POST['username']); //clean!
$arrayUsers = array($myUsername, 
‘tom’, ‘tommy’); //clean!
define(”GREETING”, ‘hello there’ . $myUsername); 
//clean!
function cleanInput($input){
$clean = 
strtolower($input);
$clean = preg_replace(”/[^a-z]/”, “”, $clean);
$clean 
= substr($clean,0,12);
return $clean;
}[/php]

規則 2:禁用那些使安全性難以實施的 PHP設定

已經知道了不能信任使用者輸入,還應該知道不應該信任機器上配置 PHP 的方式。例如,要確保禁用 register_globals。如果啟用了register_globals,就可能做一些粗心的事情,比如使用 $variable 替換同名的 GET 或 POST 字串。通過禁用這個設定,PHP強迫您在正確的名稱空間中引用正確的變數。要使用來自表單 POST 的變數,應該引用 $_POST['variable']。這樣就不會將這個特定變數誤會成cookie、會話或 GET變數。

要檢查的第二個設定是錯誤報告級別。在開發期間,希望獲得儘可能多的錯誤報告,但是在交付專案時,希望將錯誤記錄到日誌檔案中,而不是顯示在螢幕上。為什麼呢?因為惡意的黑客會使用錯誤報告資訊(比如SQL 錯誤)來猜測應用程式正在做什麼。這種偵察可以幫助黑客突破應用程式。為了堵住這個漏洞,需要編輯 php.ini 檔案,為 error_log條目提供合適的目的地,並將 display_errors 設定為 Off。

規則3:如果不能理解它,就不能保護它

一些開發人員使用奇怪的語法,或者將語句組織得很緊湊,形成簡短但是含義模糊的程式碼。這種方式可能效率高,但是如果您不理解程式碼正在做什麼,那麼就無法決定如何保護它。

例如,您喜歡下面兩段程式碼中的哪一段?

清單4. 使程式碼容易得到保護
[php]//obfuscated code
$input = 
(isset($_POST['username']) ? $_POST['username']:”);
//unobfuscated 
code
$input = ”;
if (isset($_POST['username'])){
$input = 
$_POST['username'];
}else{
$input = 
”;
}[/php]

在第二個比較清晰的程式碼段中,很容易看出 $input 是有瑕疵的,需要進行清理,然後才能安全地處理。

規則4:“縱深防禦” 是新的法寶

本教程將用示例來說明如何保護線上表單,同時在處理表單的 PHP 程式碼中採用必要的措施。同樣,即使使用 PHP regex來確保 GET 變數完全是數字的,仍然可以採取措施確保 SQL查詢使用轉義的使用者輸入。
縱深防禦不只是一種好思想,它可以確保您不會陷入嚴重的麻煩。

既然已經討論了基本規則,現在就來研究第一種威脅:SQL隱碼攻擊。

防止 SQL 注入攻擊

在 SQL 注入攻擊 中,使用者通過操縱表單或 GET查詢字串,將資訊新增到資料庫查詢中。例如,假設有一個簡單的登入資料庫。這個資料庫中的每個記錄都有一個使用者名稱欄位和一個密碼欄位。構建一個登入表單,讓使用者能夠登入。

清單5. 簡單的登入表單
[php]
Login

 Username

 Password

[/php]
這個表單接受使用者輸入的使用者名稱和密碼,並將使用者輸入提交給名為 verify.php的檔案。在這個檔案中,PHP 處理來自登入表單的資料,如下所示:

清單 6. 不安全的 PHP 表單處理程式碼
[php] $okay = 0;
$username = $_POST['user'];
$pw = 
$_POST['pw'];
$sql = “select count(*) as ctr from users 
where
username=’”.$username.”‘ and password=’”. $pw.”‘ limit 
1″;
$result = mysql_query($sql);
while ($data = 
mysql_fetch_object($result)){
if ($data->ctr == 1){
 //they’re okay to 
enter the application!
 $okay = 1;
}
}
if 
($okay){
$_SESSION['loginokay'] = 
true;
header(”index.php”);
}else{
header(”login.php”);
}
?> 
[/php]

這段程式碼看起來沒問題,對嗎?世界各地成百(甚至成千)的 PHP/MySQL 站點都在使用這樣的程式碼。它錯在哪裡?好,記住“不能信任使用者輸入”。這裡沒有對來自使用者的任何資訊進行轉義,因此使應用程式容易受到攻擊。具體來說,可能會出現任何型別的 SQL隱碼攻擊。

例如,如果使用者輸入 foo 作為使用者名稱,輸入 ‘ or ‘1′=’1 作為密碼,那麼實際上會將以下字串傳遞給 PHP,然後將查詢傳遞給MySQL:
$sql = “select count(*) as ctr  from users where
username=’foo’ 
and password=” or ‘1′=’1′ limit 1″;

這個查詢總是返回計數值 1,因此 PHP會允許進行訪問。通過在密碼字串的末尾註入某些惡意 SQL,黑客就能裝扮成合法的使用者。

解決這個問題的辦法是,將 PHP 的內建mysql_real_escape_string() 函式用作任何使用者輸入的包裝器。這個函式對字串中的字元進行轉義,使字串不可能傳遞撇號等特殊字元並讓MySQL 根據特殊字元進行操作。清單 7 展示了帶轉義處理的程式碼。

清單 7. 安全的 PHP 表單處理程式碼
[php] $okay = 0;
$username = $_POST['user'];
$pw = 
$_POST['pw'];
$sql = “select count(*) as ctr from users 
where
 username=’”.mysql_real_escape_string($username).”‘
 and 
password=’”. mysql_real_escape_string($pw).”‘ limit 1″;
 
$result = 
mysql_query($sql);
while ($data = mysql_fetch_object($result)){
if 
($data->ctr == 1){
 //they’re okay to enter the application!
 $okay = 
1;
}
}
if ($okay){
$_SESSION['loginokay'] = 
true;
header(”index.php”);
}else{
header(”login.php”);
}
?>[/php]

使用mysql_real_escape_string() 作為使用者輸入的包裝器,就可以避免使用者輸入中的任何惡意 SQL 注入。如果使用者嘗試通過 SQL隱碼攻擊傳遞畸形的密碼,那麼會將以下查詢傳遞給資料庫:
select count(*) as ctr from users where 
\
username=’foo’ and password=’\’ or \’1\’=\’1′ limit 
1″
資料庫中沒有任何東西與這樣的密碼匹配。僅僅採用一個簡單的步驟,就堵住了 Web 應用程式中的一個大漏洞。這裡得出的經驗是,總是應該對 SQL查詢的使用者輸入進行轉義。

但是,還有幾個安全漏洞需要堵住。下一項是操縱 GET 變數。

防止使用者操縱變數

在前一節中,防止了使用者使用畸形的密碼進行登入。如果您很聰明,應該應用您學到的方法,確保對 SQL語句的所有使用者輸入進行轉義。

但是,使用者現在已經安全地登入了。使用者擁有有效的密碼,並不意味著他將按照規則行事 ——他有很多機會能夠造成損害。例如,應用程式可能允許使用者檢視特殊的內容。所有連結指向 template.php?pid=33 或template.php?pid=321 這樣的位置。URL 中問號後面的部分稱為查詢字串。因為查詢字串直接放在 URL 中,所以也稱為 GET查詢字串。

在 PHP 中,如果禁用了 register_globals,那麼可以用 $_GET['pid'] 訪問這個字串。在template.php 頁面中,可能會執行與清單 8 相似的操作。

清單 8. 示例 template.php
[php] $pid = $_GET['pid'];
//we create an object of a 
fictional class Page
$obj = new Page;
$content = 
$obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the 
page
//……
//……
?> [/php]

這裡有什麼錯嗎?首先,這裡隱含地相信來自瀏覽器的 GET 變數 pid是安全的。這會怎麼樣呢?大多數使用者沒那麼聰明,無法構造出語義攻擊。但是,如果他們注意到瀏覽器的 URL 位置域中的pid=33,就可能開始搗亂。如果他們輸入另一個數字,那麼可能沒問題;但是如果輸入別的東西,比如輸入 SQL 命令或某個檔案的名稱(比如/etc/passwd),或者搞別的惡作劇,比如輸入長達 3,000個字元的數值,那麼會發生什麼呢?

在這種情況下,要記住基本規則,不要信任使用者輸入。應用程式開發人員知道 template.php接受的個人識別符號(PID)應該是數字,所以可以使用 PHP 的 is_numeric() 函式確保不接受非數字的 PID,如下所示:

清單 9. 使用is_numeric() 來限制 GET 變數
[php] $pid = 
$_GET['pid'];
if (is_numeric($pid)){
//we create an object of a fictional 
class Page
$obj = new Page;
$content = $obj->fetchPage($pid);
//and 
now we have a bunch of PHP that displays the 
page 
//……
//……
}else{
//didn’t pass the is_numeric() test, do 
something else!
}?> [/php]

這個方法似乎是有效的,但是以下這些輸入都能夠輕鬆地通過 is_numeric()的檢查:

100.1 (不應該有小數位)0xff33669f(十六進位制 —— 危險!危險!)
100 (有效)
100.1 (不應該有小數位)
+0123.45e6 (科學計數法 —— 不好)
0xff33669f 
(十六進位制 —— 危險!危險!)

那麼,有安全意識的 PHP 開發人員應該怎麼做呢?多年的經驗表明,最好的做法是使用正規表示式來確保整個 GET變數由數字組成,如下所示:

清單 10. 使用正規表示式限制 GET 變數
[php] $pid = $_GET['pid'];

if 
(strlen($pid)){
if (!ereg(”^[0-9]+$”,$pid)){
 //do something appropriate, 
like maybe logging \
 them out or sending them back to home 
page
}
}else{
//empty $pid, so send them back to the home 
page
}

//we create an object of a fictional class Page, which is 
now
//moderately protected from evil user input
$obj = new 
Page;
$content = $obj->fetchPage($pid);
//and now we have a bunch of 
PHP that displays the page 
//……
//……
?>[/php]

需要做的只是使用 strlen()檢查變數的長度是否非零;如果是,就使用一個全數字正規表示式來確保資料元素是有效的。如果 PID包含字母、斜線、點號或任何與十六進位制相似的內容,那麼這個例程捕獲它並將頁面從使用者活動中遮蔽。如果看一下 Page 類幕後的情況,就會看到有安全意識的 PHP開發人員已經對使用者輸入 $pid 進行了轉義,從而保護了 fetchPage() 方法,如下所示:

清單 11. 對 fetchPage()方法進行轉義
[php] class Page{
 function 
fetchPage($pid){
 $sql = “select pid,title,desc,kw,content,\
 status 
from page where pid=’
 ”.mysql_real_escape_string($pid).”‘”;
 //etc, 
etc….
}
}
?> [/php]

您可能會問,“既然已經確保 PID 是數字,那麼為什麼還要進行轉義?”因為不知道在多少不同的上下文和情況中會使用 fetchPage()方法。必須在呼叫這個方法的所有地方進行保護,而方法中的轉義體現了縱深防禦的意義。

如果使用者嘗試輸入非常長的數值,比如長達 1000個字元,試圖發起緩衝區溢位攻擊,那麼會發生什麼呢?下一節更詳細地討論這個問題,但是目前可以新增另一個檢查,確保輸入的 PID 具有正確的長度。您知道資料庫的pid 欄位的最大長度是 5 位,所以可以新增下面的檢查。

清單 12. 使用正規表示式和長度檢查來限制 GET 變數
[php] $pid = $_GET['pid'];
if (strlen($pid)){
if 
(!ereg(”^[0-9]+$”,$pid) && strlen($pid) > 5){
 //do something 
appropriate, like maybe logging \
 them out or sending them back to home 
page
}
}else{
//empty $pid, so send them back to the home 
page
}
//we create an object of a fictional class Page, which is 
now
//even more protected from evil user input
$obj = new 
Page;
$content = $obj->fetchPage($pid);
//and now we have a bunch of 
PHP that displays the page 
//……
//……
?> 
[/php]

現在,任何人都無法在資料庫應用程式中塞進一個 5,000 位的數值 —— 至少在涉及 GET字串的地方不會有這種情況。想像一下黑客在試圖突破您的應用程式而遭到挫折時咬牙切齒的樣子吧!而且因為關閉了錯誤報告,黑客更難進行偵察。

緩衝區溢位攻擊

緩衝區溢位攻擊試圖使 PHP 應用程式中(或者更精確地說,在 Apache 或底層作業系統中)的記憶體分配緩衝區發生溢位。請記住,您可能是使用 PHP 這樣的高階語言來編寫Web 應用程式,但是最終還是要呼叫 C(在 Apache 的情況下)。與大多數低階語言一樣,C對於記憶體分配有嚴格的規則。

緩衝區溢位攻擊向緩衝區傳送大量資料,使部分資料溢位到相鄰的記憶體緩衝區,從而破壞緩衝區或者重寫邏輯。這樣就能夠造成拒絕服務、破壞資料或者在遠端伺服器上執行惡意程式碼。

防止緩衝區溢位攻擊的惟一方法是檢查所有使用者輸入的長度。例如,如果有一個表單元素要求輸入使用者的名字,那麼在這個域上新增值為40 的 maxlength 屬性,並在後端使用 substr() 進行檢查。清單 13 給出表單和 PHP 程式碼的簡短示例。

清單 13.檢查使用者輸入的長度
[php] if ($_POST['submit'] 
== “go”){
$name = substr($_POST['name'],0,40);
//continue 
processing….
}
?>

<form action="http://www.php1.cn/Content/%E2%80%9D%3C?php" \
為什麼既提供 maxlength 屬性,又在後端進行 substr()檢查?因為縱深防禦總是好的。瀏覽器防止使用者輸入 PHP 或 MySQL 不能安全地處理的超長字串(想像一下有人試圖輸入長達 1,000 個字元的名稱),而後端PHP 檢查會確保沒有人遠端地或者在瀏覽器中操縱表單資料。

<form action="http://www.php1.cn/Content/%E2%80%9D%3C?php" \正如您看到的,這種方式與前一節中使用 strlen() 檢查 GET 變數 pid的長度相似。在這個示例中,忽略長度超過 5 位的任何輸入值,但是也可以很容易地將值截短到適當的長度,如下所示:

清單 14. 改變輸入的 GET變數的長度
[php] $pid = $_GET['pid'];
if 
(strlen($pid)){
if (!ereg(”^[0-9]+$”,$pid)){
 //if non numeric $pid, send 
them back to home page
}
}else{
//empty $pid, so send them back to the 
home page
}
//we have a numeric pid, but it may be too long, so let’s 
check
if (strlen($pid)>5){
 $pid = substr($pid,0,5);
}
//we 
create an object of a fictional class Page, which is now
//even more 
protected from evil user input
$obj = new Page;
$content = 
$obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the 
page 
//……
//……
?>[/php]

注意,緩衝區溢位攻擊並不限於長的數字串或字母串。也可能會看到長的十六進位制字串(往往看起來像\xA3 或\xFF)。記住,任何緩衝區溢位攻擊的目的都是淹沒特定的緩衝區,並將惡意程式碼或指令放到下一個緩衝區中,從而破壞資料或執行惡意程式碼。對付十六進位制緩衝區溢位最簡單的方法也是不允許輸入超過特定的長度。

如果您處理的是允許在資料庫中輸入較長條目的表單文字區,那麼無法在客戶端輕鬆地限制資料的長度。在資料到達PHP 之後,可以使用正規表示式清除任何像十六進位制的字串。

清單 15. 防止十六進位制字串
[php] if ($_POST['submit'] == “go”){
$name = 
substr($_POST['name'],0,40);
//clean out any potential hexadecimal 
characters
$name = cleanHex($name);
//continue 
processing….
}
function cleanHex($input){
$clean = 
preg_replace(”![\][xX]([A-Fa-f0-9]{1,3})!”, “”,$input);
return 
$clean;
}
?>
” method=”post”>
 Name

[/php]

您可能會發現這一系列操作有點兒太嚴格了。畢竟,十六進位制串有合法的用途,比如輸出外語中的字元。如何部署十六進位制regex 由您自己決定。比較好的策略是,只有在一行中包含過多十六進位制串時,或者字串的字元超過特定數量(比如 128 或255)時,才刪除十六進位制串。

跨站點指令碼攻擊

在跨站點指令碼(XSS)攻擊中,往往有一個惡意使用者在表單中(或通過其他使用者輸入方式)輸入資訊,這些輸入將惡意的客戶端標記插入過程或資料庫中。例如,假設站點上有一個簡單的來客登記簿程式,讓訪問者能夠留下姓名、電子郵件地址和簡短的訊息。惡意使用者可以利用這個機會插入簡短訊息之外的東西,比如對於其他使用者不合適的圖片或將使用者重定向到另一個站點的JavaScript,或者竊取 cookie 資訊。

幸運的是,PHP 提供了 strip_tags() 函式,這個函式可以清除任何包圍在 HTML標記中的內容。strip_tags() 函式還允許提供允許標記的列表,比如 或 

清單 16給出一個示例,這個示例是在前一個示例的基礎上構建的。

清單 16. 從使用者輸入中清除 HTML 標記
[php] if ($_POST['submit'] == 
“go”){
//strip_tags
$name = strip_tags($_POST['name']);
$name = 
substr($name,0,40);
//clean out any potential hexadecimal characters
$name 
= cleanHex($name);
//continue processing….
}
function 
cleanHex($input){
$clean = preg_replace\
(”![\][xX]([A-Fa-f0-9]{1,3})!”, 
“”,$input);
return $clean;
}
?>
“” 
method=”post”>
 
Name
“text” name=”name” id=”name” size=”20″ maxlength=”40″/>


[/php] 


從安全的角度來看,對公共使用者輸入使用 strip_tags()是必要的。如果表單在受保護區域(比如內容管理系統)中,而且您相信使用者會正確地執行他們的任務(比如為 Web 站點建立 HTML 內容),那麼使用strip_tags()可能是不必要的,會影響工作效率。

還有一個問題:如果要接受使用者輸入,比如對貼子的評論或來客登記項,並需要將這個輸入向其他使用者顯示,那麼一定要將響應放在PHP 的 htmlspecialchars() 函式中。這個函式將與符號、< 和 > 符號轉換為 HTML 實體。例如,與符號(&)變成&。這樣的話,即使惡意內容躲開了前端 strip_tags() 的處理,也會在後端被 htmlspecialchars()處理掉。

瀏覽器內的資料操縱

有一類瀏覽器外掛允許使用者篡改頁面上的頭部元素和表單元素。使用 Tamper Data(一個Mozilla 外掛),可以很容易地操縱包含許多隱藏文字欄位的簡單表單,從而向 PHP 和 MySQL 傳送指令。

使用者在點選表單上的 Submit之前,他可以啟動 Tamper Data。在提交表單時,他會看到表單資料欄位的列表。Tamper Data允許使用者篡改這些資料,然後瀏覽器完成表單提交。

讓我們回到前面建立的示例。已經檢查了字串長度、清除了 HTML標記並刪除了十六進位制字元。但是,新增了一些隱藏的文字欄位,如下所示:

清單 17. 隱藏變數
[php] if ($_POST['submit'] ==“go”){
//strip_tags
$name = strip_tags($_POST['name']);
$name =substr($name,0,40);
//clean out any potential hexadecimal characters
$name= cleanHex($name);
//continue processing….
}
functioncleanHex($input){
$clean = \
preg_replace(”![\][xX]([A-Fa-f0-9]{1,3})!”,“”,$input);
return $clean;
}
?>
””method=”post”>
Name
“text” name=”name” id=”name” size=”20″ maxlength=”40″/>

[/php]
注意,隱藏變數之一暴露了表名:users。還會看到一個值為 create 的 action欄位。只要有基本的 SQL 經驗,就能夠看出這些命令可能控制著中介軟體中的一個 SQL 引擎。想搞大破壞的人只需改變表名或提供另一個選項,比如delete。

圖 1 說明了 Tamper Data 能夠提供的破壞範圍。注意,Tamper Data 不但允許使用者訪問表單資料元素,還允許訪問HTTP 頭和 cookie。

圖 1. Tamper Data 視窗



要防禦這種工具,最簡單的方法是假設任何使用者都可能使用 TamperData(或類似的工具)。只提供系統處理表單所需的最少量的資訊,並把表單提交給一些專用的邏輯。例如,登錄檔單應該只提交給註冊邏輯。

如果已經建立了一個通用表單處理函式,有許多頁面都使用這個通用邏輯,那該怎麼辦?如果使用隱藏變數來控制流向,那該怎麼辦?例如,可能在隱藏表單變數中指定寫哪個資料庫表或使用哪個檔案儲存庫。有4 種選擇:

不改變任何東西,暗自祈禱系統上沒有任何惡意使用者。

重寫功能,使用更安全的專用表單處理函式,避免使用隱藏表單變數。

使用md5() 或其他加密機制對隱藏表單變數中的表名或其他敏感資訊進行加密。在 PHP 端不要忘記對它們進行解密。

通過使用縮寫或暱稱讓值的含義模糊,在PHP 表單處理函式中再對這些值進行轉換。例如,如果要引用 users 表,可以用 u 或任意字串(比如u8y90×0jkL)來引用它。

後兩個選項並不完美,但是與讓使用者輕鬆地猜出中介軟體邏輯或資料模型相比,它們要好得多了。

現在還剩下什麼問題呢?遠端表單提交。

遠端表單提交

Web的好處是可以分享資訊和服務。壞處也是可以分享資訊和服務,因為有些人做事毫無顧忌。

以表單為例。任何人都能夠訪問一個 Web 站點,並使用瀏覽器上的File > Save As 建立表單的本地副本。然後,他可以修改 action 引數來指向一個完全限定的 URL(不指向formHandler.php,而是指向http://www.yoursite.com/formHandler.php,因為表單在這個站點上),做他希望的任何修改,點選Submit,伺服器會把這個表單資料作為合法通訊流接收。

首先可能考慮檢查$_SERVER['HTTP_REFERER'],從而判斷請求是否來自自己的伺服器,這種方法可以擋住大多數惡意使用者,但是擋不住最高明的黑客。這些人足夠聰明,能夠篡改頭部中的引用者資訊,使表單的遠端副本看起來像是從您的伺服器提交的。
處理遠端表單提交更好的方式是,根據一個惟一的字串或時間戳生成一個令牌,並將這個令牌放在會話變數和表單中。提交表單之後,檢查兩個令牌是否匹配。如果不匹配,就知道有人試圖從表單的遠端副本傳送資料。

要建立隨機的令牌,可以使用PHP 內建的 md5()、uniqid() 和 rand() 函式,如下所示:

清單 18. 防禦遠端表單提交
[php] session_start();
if ($_POST['submit'] ==“go”){
//check token
if ($_POST['token'] ==$_SESSION['token']){
//strip_tags
$name =strip_tags($_POST['name']);
$name = substr($name,0,40);
//clean outany potential hexadecimal characters
$name =cleanHex($name);
//continue processing….
}else{
//stop allprocessing! remote form posting attempt!
}
}
$token =md5(uniqid(rand(), true));
$_SESSION['token']= $token;
functioncleanHex($input){
$clean = preg_replace(”![\][xX]([A-Fa-f0-9]{1,3})!”,“”,$input);
return $clean;
}
?>
” method=”post”>
Name

”/>

[/php]

這種技術是有效的,這是因為在 PHP 中會話資料無法在伺服器之間遷移。即使有人獲得了您的 PHP原始碼,將它轉移到自己的伺服器上,並向您的伺服器提交資訊,您的伺服器接收的也只是空的或畸形的會話令牌和原來提供的表單令牌。它們不匹配,遠端表單提交就失敗了。

結束語

本教程討論了許多問題:

  • 使用mysql_real_escape_string() 防止 SQL 注入問題。
  • 使用正規表示式和 strlen() 來確保 GET資料未被篡改。
  • 使用正規表示式和 strlen() 來確保使用者提交的資料不會使記憶體緩衝區溢位。
  • 使用 strip_tags() 和htmlspecialchars() 防止使用者提交可能有害的 HTML 標記。
  • 避免系統被 Tamper Data這樣的工具突破。
  • 使用惟一的令牌防止使用者向伺服器遠端提交表單。


本教程沒有涉及更高階的主題,比如檔案注入、HTTP頭欺騙和其他漏洞。但是,您學到的知識可以幫助您馬上增加足夠的安全性,使當前專案更安全。
評論(3)

相關文章