原則:
1.2.1. 深度防範
深度防範原則是安全專業人員人人皆知的原則,它說明了冗餘安全措施的價值,這是被曆史所證明的。
深度防範原則可以延伸到其它領域,不僅僅是局限於編程領域。使用過備份傘的跳傘隊員可以證明有冗餘安全措施是多麼的有價值,儘管大家永遠不希望主傘失效。一個冗餘的安全措施可以在主安全措施失效的潛在的起到重大作用。
回到編程領域,堅持深度防範原則要求您時刻有一個備份方案。如果一個安全措施失效了,必須有另外一個提供一些保護。例如,在使用者進行重要操作前進行重新使用者認證就是一個很好的習慣,儘管你的使用者認證邏輯裡面沒有已知缺陷。如果一個未認證使用者通過某種方法偽裝成另一個使用者,提示錄入密碼可以潛在地避免未認證(未驗證)使用者進行一些關鍵操作。
儘管深度防範是一個合理的原則,但是過度地增加安全措施只能增加成本和降低價值。
1.2.2. 最小許可權
我過去有一輛汽車有一個傭人鑰匙。這個鑰匙只能用來點火,所以它不能開啟車門、控制台、後備箱,它只能用來啟動汽車。我可以把它給泊車員(或把它留在點火器上),我確認這個鑰匙不能用於其它目的。
把一個不能開啟控制台或後備箱的鑰匙給泊車員是有道理的,畢竟,你可能想在這些地方儲存貴重物品。但我覺得沒有道理的是為什麼它不能開車門。當然,這是因為我的觀點是在於許可權的收回。我是在想為什麼泊車員被取消了開車門的許可權。在編程中,這是一個很不好的觀點。相反地,你應該考慮什麼許可權是必須的,只能給予每個人完成他本職工作所必須的盡量少的許可權。
一個為什麼傭人鑰匙不能開啟車門的理由是這個鑰匙可以被複製,而這個複製的鑰匙在將來可能被用於偷車。這個情況聽起來不太可能發生,但這個例子說明了不必要的授權會加大你的風險,即使是增加了很小許可權也會如此。風險最小化是安全程式開發的主要組成部分。
你無需去考慮一項許可權被濫用的所有方法。事實上,你要預測每一個潛在攻擊者的動作是幾乎不可能的。
1.2.3. 簡單就是美
複雜滋生錯誤,錯誤能導致安全性漏洞。這個簡單的事實說明了為什麼簡單對於一個安全的應用來說是多麼重要。沒有必要的複雜與沒有必要的風險一樣糟糕。
例如,下面的代碼摘自一個最近的安全性漏洞通告:
CODE:
$search = (isset($_GET['search']) ? $_GET['search'] : '');
?>
這個流程會混淆$search變數受汙染*的事實,特別是對於缺乏經驗的開發人員而言。上面語句等價於下面的程式:
CODE:
$search = '';
if (isset($_GET['search'])){
$search = $_GET['search'];
}
?>
上面的兩個處理流程是完全相同的。現在請注意一下下面的語句:
$search = $_GET['search'];
使用這一語句,在不影響流程的情況下,保證了$search變數的狀態維持原樣,同時還可以看出它是否受汙染。
* 譯註:受汙染變數,即在程式執行過程中,該變數的值不是由指派陳述式直接指定值,而是來自其它來源,如控制台錄入、資料庫等。
1.2.4. 暴露最小化
PHP應用程式需要在PHP與外部資料源間進行頻繁通訊。主要的外部資料源是用戶端瀏覽器和資料庫。如果你正確的跟蹤資料,你可以確定哪些資料被暴露了。Internet是最主要的暴露源,這是因為它是一個非常公用的網路,您必須時刻小心防止資料被暴露在Internet上。
資料暴露不一定就意味著安全風險。可是資料暴露必須盡量最小化。例如,一個使用者進入支付系統,在向你的伺服器傳輸他的信用卡資料時,你應該用SSL去保護它。如果你想要在一個確認頁面上顯示他的信用卡號時,由於該卡號資訊是由伺服器發向他的用戶端的,你同樣要用SSL去保護它。
再談談上一小節的例子,顯示信用卡號顯然增加了暴露的機率。SSL確實可以降低風險,但是最佳的解決方案是通過只顯示最後四位元,從而達到徹底杜絕風險的目的。
為了降低對敏感性資料的暴露率,你必須確認什麼資料是敏感的,同時跟蹤它,並消除所有不必要的資料暴露。在本書中,我會展示一些技巧,用以協助你實現對很多常見敏感性資料的保護。
方法:
1.3.1. 平衡風險與可用性
使用者操作的友好性與安全措施是一對矛盾,在提高安全性的同時,通常會降低可用性。在你為不合邏輯的使用者寫代碼時,必須要考慮到符合邏輯的正常使用者。要達到適當的平衡的確很難,但是你必須去做好它,沒有人能替代你,因為這是你的軟體。
盡量使安全措施對使用者透明,使他們感受不到它的存在。如果實在不可能,就盡量採用使用者比較常見和熟悉的方式來進行。例如,在使用者訪問受控資訊或服務前讓他們輸入使用者名稱和密碼就是一種比較好的方式。
當你懷疑可能有非法操作時,必須意識到你可能會搞借。例如,在使用者操作時如果系統對使用者身份有疑問時,通常用讓使用者再次錄入密碼。這對於合法使用者來說只是稍有不便,而對於攻擊者來說則是銅牆鐵壁。從技術上來說,這與提示使用者進行重新登入基本是一樣的,但是在使用者感受上,則有天壤之別。
沒有必要將使用者踢出系統並指責他們是所謂的攻擊者。當你犯錯時,這些流程會極大的降低系統的可用性,而錯誤是難免的。
在本書中,我著重介紹透明和常用的安全措施,同時我建議大家對疑似攻擊行為做出小心和明智的反應。
1.3.2. 跟蹤資料
作為一個有安全意識的開發人員,最重要的一件事就是隨時跟蹤資料。不只是要知道它是什麼和它在哪裡,還要知道它從哪裡來,要到哪裡去。有時候要做到這些是困難的,特別是當你對WEB的運做原理沒有深入理解時。這也就是為什麼儘管有些開發人員在其它開發環境中很有經驗,但他對WEB不是很有經驗時,經常會犯錯並製造安全性漏洞。
大多數人在讀取EMAIL時,一般不會被題為"Re: Hello"之類的垃圾郵件所欺騙,因為他們知道,這個看起來像回複的主題是能被偽造的。因此,這封郵件不一定是對前一封主題為"Hello."的郵件的回複。簡而言之,人們知道不能對這個主題不能太信任。但是很少有人意識到寄件者地址也能被偽造,他們錯誤地認為它能可靠地顯示這個EMAIL的來源。
Web也非常類似,我想教給大家的其中一點是如何區分可信的和不可信的資料。做到這一點常常是不容易的,盲目的猜測並不是辦法。
PHP通過超級全域數組如$_GET, $_POST, 及$_COOKIE清楚地表示了使用者資料的來源。一個嚴格的命名體系能保證你在程式碼的任何部分知道所有資料的來源,這也是我一直所示範和強調的。
知道資料在哪裡進入你的程式是極為重要的,同時知道資料在哪裡離開你的程式也很重要。例如,當你使用echo指令時,你是在向用戶端發送資料;當你使用MySQL_query時,你是在向MySQL資料庫發送資料(儘管你的目的可能是取資料)。
在我審核PHP代碼是否有安全性漏洞時,我主要檢查代碼中與外部系統互動的部分。這部分代碼很有可能包含安全性漏洞,因此,在開發與代碼檢查時必須要加以特別仔細的注意。
1.3.3. 過濾輸入
過濾是Web應用安全的基礎。它是你驗證資料合法性的過程。通過在輸入時確認對所有的資料進行過濾,你可以避免被汙染(未過濾)資料在你的程式中被誤信及誤用。大多數流行的PHP應用的漏洞最終都是因為沒有對輸入進行恰當過濾造成的。
我所指的過濾輸入是指三個不同的步驟:
l 識別輸入
l 過濾輸入
l 區分已過濾及被汙染資料
把識別輸入做為第一步是因為如果你不知道它是什麼,你也就不能正確地過濾它。輸入是指所有源自外部的資料。例如,所有發自用戶端的是輸入,但用戶端並不是唯一的外部資料源,其它如資料庫和rss推送等也是外部資料源。
由使用者輸入的資料非常容易識別,PHP用兩個超級公用數組$_GET 和$_POST來存放使用者輸入資料。其它的輸入要難識別得多,例如,$_SERVER數組中的很多元素是由用戶端所操縱的。常常很難確認$_SERVER 數組中的哪些元素組成了輸入,所以,最好的方法是把整個數組看成輸入。
在某些情況下,你把什麼作為輸入取決於你的觀點。例如,session資料被儲存在伺服器上,你可能不會認為session資料是一個外部資料源。如果你持這種觀點的話,可以把session資料的儲存位置是在你的軟體的內部。意識到session的儲存位置的安全與軟體的安全是聯絡在一起的事實是非常明智的。同樣的觀點可以推及到資料庫,你也可以把它看成你軟體的一部分。
一般來說,把session儲存位置與資料庫看成是輸入是更為安全的,同時這也是我在所有重要的PHP應用開發中所推薦的方法。
一旦識別了輸入,你就可以過濾它了。過濾是一個有點正式的術語,它在平時表述中有很多同義字,如驗證、清潔及淨化。儘管這些大家平時所用的術語稍有不同,但它們都是指的同一個處理:防止非法資料進入你的應用。
有很多種方法過濾資料,其中有一些安全性較高。最好的方法是把過濾看成是一個檢查的過程。請不要試圖好心地去糾正非法資料,要讓你的使用者按你的規則去做,曆史證明了試圖糾正非法資料往往會導致安全性漏洞。例如,考慮一下下面的試圖防止目錄跨越的方法(訪問上層目錄)。
CODE:
$filename = str_replace('..', '.', $_POST['filename']);
?>
你能想到$_POST['filename']如何取值以使$filename成為linux系統中使用者口令檔案的路徑../../etc/passwd嗎?
答案很簡單:
.../.../etc/passwd
這個特定的錯誤可以通過反覆替換直至找不到為止:
CODE:
$filename = $_POST['filename'];
while (strpos($_POST['filename'], '..') != = FALSE){
$filename = str_replace('..', '.', $filename);
}
?>
當然,函數basename( )可以替代上面的所有邏輯,同時也能更安全地達到目的。不過重要點是在於任何試圖糾正非法資料的舉動都可能導致潛在錯誤並允許非法資料通過。只做檢查是一個更安全的選擇。
譯註:這一點深有體會,在實際項目曾經遇到過這樣一件事,是對一個使用者註冊和登入系統變更,客戶希望使用者名稱前後有空格就不能登入,結果修改時對使用者登入程式進行了更改,用trim()函數把輸入的使用者名稱前後的空格去掉了(典型的好心辦壞事),但是在註冊時居然還是允許前後有空格!結果可想而知。
除了把過濾做為一個檢查過程之外,你還可以在可能時用白名單方法。它是指你需要假定你正在檢查的資料是非法的,除非你能證明它是合法的。換而言之,你寧可在小心上犯錯。使用這個方法,一個錯誤只會導致你把合法的資料當成是非法的。儘管不想犯任何錯誤,但這樣總比把非法資料當成合法資料要安全得多。通過減輕犯錯引起的損失,你可以提高你的應用的安全性。儘管這個想法在理論上是很自然的,但曆史證明,這是一個很有價值的方法。
如果你能正確可靠地識別和過濾輸入,你的工作就基本完成了。最後一步是使用一個命名規範或其它可以協助你正確和可靠地區分已過濾和被汙染資料的方法。我推薦一個比較簡單的命名規範,因為它可以同時用在面向過程和物件導向的編程中。我用的命名規範是把所有經過濾的資料放入一個叫$clean的資料中。你需要用兩個重要的步驟來防止被汙染資料的注入:
l 經常初始化$clean為一個空數組。
l 加入檢查及阻止來自外部資料源的變數命名為clean,
實際上,只有初始化是至關緊要的,但是養成這樣一個習慣也是很好的:把所有命名為clean的變數認為是你的已過濾資料數組。這一步驟合理地保證了$clean中只包括你有意儲存進去的資料,你所要負責的只是不在$clean存在被汙染資料。
為了鞏固這些概念,考慮下面的表單,它允許使用者選擇三種顏色中的一種;
CODE:
在處理這個表單的編程邏輯中,非常容易犯的錯誤是認為只能提交三個選擇中的一個。在第二章中你將學到,用戶端能提交任何資料作為$_POST['color']的值。為了正確地過濾資料,你需要用一個switch語句來進行:
CODE:
$clean = array( );
switch($_POST['color']) {
case 'red':
case 'green':
case 'blue':
$clean['color'] = $_POST['color'];
break;
}
?>
本例中首先初始化了$clean為空白數組以防止包含被汙染的資料。一旦證明$_POST['color']是red, green, 或blue中的一個時,就會儲存到$clean['color']變數中。因此,可以確信$clean['color']變數是合法的,從而在代碼的其它部分使用它。當然,你還可以在switch結構中加入一個default分支以處理非法資料的情況。一種可能是再次顯示表單並提示錯誤。特別小心不要試圖為了友好而輸出被汙染的資料。
上面的方法對於過濾有一組已知的合法值的資料很有效,但是對於過濾有一組已知合法字元組成的資料時就沒有什麼協助。例如,你可能需要一個使用者名稱只能由字母及數字組成:
CODE:
$clean = array( );
if (ctype_alnum($_POST['username'])) {
$clean['username'] = $_POST['username'];
}
?>
儘管在這種情況下可以用Regex,但使用PHP內建函數是更完美的。這些函數包含錯誤的可能性要比你自已寫的代碼出錯的可能性要低得多,而且在過濾邏輯中的一個錯誤幾乎就意味著一個安全性漏洞。
1.3.4. 輸出轉義
另外一個Web應用安全的基礎是對輸出進行轉義或對特殊字元進行編碼,以保證原意不變。例如,O'Reilly在傳送給MySQL資料庫前需要轉義成O\'Reilly。單引號前的反斜線代表單引號是資料本身的一部分,而不是並不是它的本義。
我所指的輸出轉義具體分為三步:
l 識別輸出
l 輸出轉義
l 區分已轉義與未轉義資料
只對已過濾資料進行轉義是很有必要的。儘管轉義能防止很多常見安全性漏洞,但它不能替代輸入過濾。被汙染資料必須首先過濾然後轉義。
在對輸出進行轉義時,你必須先識別輸出。通常,這要比識別輸入簡單得多,因為它依賴於你所進行的動作。例如,識別到用戶端的輸出時,你可以在代碼中尋找下列語句:
echo
print
printf
作為一項應用的開發人員,你必須知道每一個向外部系統輸出的地方。它們構成了輸出。
象過濾一樣,轉義過程在依情形的不同而不同。過濾對於不同類型的資料處理方法也是不同的,轉義也是根據你傳輸資訊到不同的系統而採用不同的方法。
對於一些常見的輸出目標(包括用戶端、資料庫和URL)的轉義,PHP中有內建函數可用。如果你要寫一個自己演算法,做到萬無一失很重要。需要找到在外系統中特殊字元的可靠和完整的列表,以及它們的表示方式,這樣資料是被保留下來而不是轉譯了。
最常見的輸出目標是客戶機,使用htmlentities( )在資料發出前進行轉義是最好的方法。與其它字串函數一樣,它輸入是一個字串,對其進行加工後進行輸出。但是使用htmlentities( )函數的最佳方式是指定它的兩個選擇性參數:引號的轉義方式(第二參數)及字元集(第三參數)。引號的轉義方式應該指定為ENT_QUOTES,它的目的是同時轉義單引號和雙引號,這樣做是最徹底的,字元集參數必須與該頁面所使用的字元集相必配。
為了區分資料是否已轉義,我還是建議定義一個命名機制。對於輸出到客戶機的轉義資料,我使用$html數組進行儲存,該資料首先初始化成一個空數組,對所有已過濾和已轉義資料進行儲存。
CODE:
$html = array( );
$html['username'] = htmlentities($clean['username'], ENT_QUOTES, 'UTF-8');
echo "
Welcome back, {$html['username']}.
";
?>
小提示
htmlspecialchars( )函數與htmlentities( )函數基本相同,它們的參數定義完全相同,只不過是htmlentities( )的轉義更為徹底。
通過$html['username']把username輸出到用戶端,你就可以確保其中的特殊字元不會被瀏覽器所錯誤解釋。如果username只包含字母和數位話,實際上轉義是沒有必要的,但是這體現了深度防範的原則。轉義任何的輸出是一個非常好的習慣,它可以戲劇性地提高你的軟體的安全性。
另外一個常見的輸出目標是資料庫。如果可能的話,你需要對SQL語句中的資料使用PHP內建函數進行轉義。對於MySQL使用者,最好的轉義函數是 mysql_real_escape_string( )。如果你使用的資料庫沒有PHP內建轉義函數可用的話,addslashes( )是最後的選擇。
下面的例子說明了對於MySQL資料庫的正確的轉義技巧:
CODE:
$mysql = array( );
$mysql['username'] = mysql_real_escape_string($clean['username']);
$sql = "SELECT *
FROM profile
WHERE username = '{$mysql['username']}'";
$result = mysql_query($sql);
?>