程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> PHP綜合 >> PHP安全基礎方法與原則詳解

PHP安全基礎方法與原則詳解

編輯:PHP綜合

本文詳解PHP安全基礎方法與原則

原則:
1.2.1. 深度防范
       深度防范原則是安全專業人員人人皆知的原則,它說明了冗余安全措施的價值,這是被歷史所證明的。
       深度防范原則可以延伸到其它領域,不僅僅是局限於編程領域。使用過備份傘的跳傘隊員可以證明有冗余安全措施是多麼的有價值,盡管大家永遠不希望主傘失效。一個冗余的安全措施可以在主安全措施失效的潛在的起到重大作用。
       回到編程領域,堅持深度防范原則要求您時刻有一個備份方案。如果一個安全措施失效了,必須有另外一個提供一些保護。例如,在用戶進行重要操作前進行重新用戶認證就是一個很好的習慣,盡管你的用戶認證邏輯裡面沒有已知缺陷。如果一個未認證用戶通過某種方法偽裝成另一個用戶,提示錄入密碼可以潛在地避免未認證(未驗證)用戶進行一些關鍵操作。
       盡管深度防范是一個合理的原則,但是過度地增加安全措施只能增加成本和降低價值。
1.2.2. 最小權限
       我過去有一輛汽車有一個傭人鑰匙。這個鑰匙只能用來點火,所以它不能打開車門、控制台、後備箱,它只能用來啟動汽車。我可以把它給泊車員(或把它留在點火器上),我確認這個鑰匙不能用於其它目的。
       把一個不能打開控制台或後備箱的鑰匙給泊車員是有道理的,畢竟,你可能想在這些地方保存貴重物品。但我覺得沒有道理的是為什麼它不能開車門。當然,這是因為我的觀點是在於權限的收回。我是在想為什麼泊車員被取消了開車門的權限。在編程中,這是一個很不好的觀點。相反地,你應該考慮什麼權限是必須的,只能給予每個人完成他本職工作所必須的盡量少的權限。
       一個為什麼傭人鑰匙不能打開車門的理由是這個鑰匙可以被復制,而這個復制的鑰匙在將來可能被用於偷車。這個情況聽起來不太可能發生,但這個例子說明了不必要的授權會加大你的風險,即使是增加了很小權限也會如此。風險最小化是安全程序開發的主要組成部分。
       你無需去考慮一項權限被濫用的所有方法。事實上,你要預測每一個潛在攻擊者的動作是幾乎不可能的。
1.2.3. 簡單就是美
       復雜滋生錯誤,錯誤能導致安全漏洞。這個簡單的事實說明了為什麼簡單對於一個安全的應用來說是多麼重要。沒有必要的復雜與沒有必要的風險一樣糟糕。

例如,下面的代碼摘自一個最近的安全漏洞通告:

CODE:

  <?PHP
  $search = (isset($_GET['search']) ? $_GET['search'] : '');
?>

       這個流程會混淆$search變量受污染*的事實,特別是對於缺乏經驗的開發者而言。上面語句等價於下面的程序:

CODE:

  <?PHP
  $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:

<?PHP
  $filename = str_replace('..', '.', $_POST['filename']);
  ?>

       你能想到$_POST['filename']如何取值以使$filename成為Linux系統中用戶口令文件的路徑../../etc/passwd嗎?

       答案很簡單:
  .../.../etc/passwd

       這個特定的錯誤可以通過反復替換直至找不到為止:

CODE:

  <?PHP
  $filename = $_POST['filename'];
  while (strpos($_POST['filename'], '..') !=  = FALSE){
    $filename = str_replace('..', '.', $filename);
  }
  ?>

       當然,函數basename( )可以替代上面的所有邏輯,同時也能更安全地達到目的。不過重要點是在於任何試圖糾正非法數據的舉動都可能導致潛在錯誤並允許非法數據通過。只做檢查是一個更安全的選擇。

      譯注:這一點深有體會,在實際項目曾經遇到過這樣一件事,是對一個用戶注冊和登錄系統進行更改,客戶希望用戶名前後有空格就不能登錄,結果修改時對用戶登錄程序進行了更改,用trim()函數把輸入的用戶名前後的空格去掉了(典型的好心辦壞事),但是在注冊時居然還是允許前後有空格!結果可想而知。
       除了把過濾做為一個檢查過程之外,你還可以在可能時用白名單方法。它是指你需要假定你正在檢查的數據是非法的,除非你能證明它是合法的。換而言之,你寧可在小心上犯錯。使用這個方法,一個錯誤只會導致你把合法的數據當成是非法的。盡管不想犯任何錯誤,但這樣總比把非法數據當成合法數據要安全得多。通過減輕犯錯引起的損失,你可以提高你的應用的安全性。盡管這個想法在理論上是很自然的,但歷史證明,這是一個很有價值的方法。
       如果你能正確可靠地識別和過濾輸入,你的工作就基本完成了。最後一步是使用一個命名約定或其它可以幫助你正確和可靠地區分已過濾和被污染數據的方法。我推薦一個比較簡單的命名約定,因為它可以同時用在面向過程和面向對象的編程中。我用的命名約定是把所有經過濾的數據放入一個叫$clean的數據中。你需要用兩個重要的步驟來防止被污染數據的注入

l        經常初始化$clean為一個空數組。
l        加入檢查及阻止來自外部數據源的變量命名為clean,

       實際上,只有初始化是至關緊要的,但是養成這樣一個習慣也是很好的:把所有命名為clean的變量認為是你的已過濾數據數組。這一步驟合理地保證了$clean中只包括你有意保存進去的數據,你所要負責的只是不在$clean存在被污染數據。

       為了鞏固這些概念,考慮下面的表單,它允許用戶選擇三種顏色中的一種;
CODE:

<form action="PRocess.PHP" method="POST">
  Please select a color:
  <select name="color">
    <option value="red">red</option>
    <option value="green">green</option>
    <option value="blue">blue</option>
  </select>
  <input type="submit" />
  </form>

       在處理這個表單的編程邏輯中,非常容易犯的錯誤是認為只能提交三個選擇中的一個。在第二章中你將學到,客戶端能提交任何數據作為$_POST['color']的值。為了正確地過濾數據,你需要用一個switch語句來進行:
CODE:

  <?PHP
  $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:

<?PHP
  $clean = array(  );
  if (ctype_alnum($_POST['username'])) {
    $clean['username'] = $_POST['username'];
  }
  ?>
       盡管在這種情況下可以用正則表達式,但使用PHP內置函數是更完美的。這些函數包含錯誤的可能性要比你自已寫的代碼出錯的可能性要低得多,而且在過濾邏輯中的一個錯誤幾乎就意味著一個安全漏洞。
1.3.4. 輸出轉義
       另外一個Web應用安全的基礎是對輸出進行轉義或對特殊字符進行編碼,以保證原意不變。例如,O'Reilly在傳送給MySQL數據庫前需要轉義成O\'Reilly。單引號前的反斜槓代表單引號是數據本身的一部分,而不是並不是它的本義。
       我所指的輸出轉義具體分為三步:
l        識別輸出
l        輸出轉義
l        區分已轉義與未轉義數據

      只對已過濾數據進行轉義是很有必要的。盡管轉義能防止很多常見安全漏洞,但它不能替代輸入過濾。被污染數據必須首先過濾然後轉義。
      在對輸出進行轉義時,你必須先識別輸出。通常,這要比識別輸入簡單得多,因為它依賴於你所進行的動作。例如,識別到客戶端的輸出時,你可以在代碼中查找下列語句:

echo
print
printf
<?=

       作為一項應用的開發者,你必須知道每一個向外部系統輸出的地方。它們構成了輸出。
       象過濾一樣,轉義過程在依情形的不同而不同。過濾對於不同類型的數據處理方法也是不同的,轉義也是根據你傳輸信息到不同的系統而采用不同的方法。
       對於一些常見的輸出目標(包括客戶端、數據庫和URL)的轉義,PHP中有內置函數可用。如果你要寫一個自己算法,做到萬無一失很重要。需要找到在外系統中特殊字符的可靠和完整的列表,以及它們的表示方式,這樣數據是被保留下來而不是轉譯了。
       最常見的輸出目標是客戶機,使用HtmlentitIEs( )在數據發出前進行轉義是最好的方法。與其它字符串函數一樣,它輸入是一個字符串,對其進行加工後進行輸出。但是使用HtmlentitIEs( )函數的最佳方式是指定它的兩個可選參數:引號的轉義方式(第二參數)及字符集(第三參數)。引號的轉義方式應該指定為ENT_QUOTES,它的目的是同時轉義單引號和雙引號,這樣做是最徹底的,字符集參數必須與該頁面所使用的字符集相必配。
      為了區分數據是否已轉義,我還是建議定義一個命名機制。對於輸出到客戶機的轉義數據,我使用$Html數組進行存儲,該數據首先初始化成一個空數組,對所有已過濾和已轉義數據進行保存。
CODE:

<?PHP
  $Html = array(  );
  $html['username'] = HtmlentitIEs($clean['username'], ENT_QUOTES, 'UTF-8');
  echo "<p>Welcome back, {$Html['username']}.</p>";
  ?>

小提示
       htmlspecialchars( )函數與htmlentitIEs( )函數基本相同,它們的參數定義完全相同,只不過是HtmlentitIEs( )的轉義更為徹底。
       通過$Html['username']把username輸出到客戶端,你就可以確保其中的特殊字符不會被浏覽器所錯誤解釋。如果username只包含字母和數字的話,實際上轉義是沒有必要的,但是這體現了深度防范的原則。轉義任何的輸出是一個非常好的習慣,它可以戲劇性地提高你的軟件的安全性。
       另外一個常見的輸出目標是數據庫。如果可能的話,你需要對SQL語句中的數據使用PHP內建函數進行轉義。對於MySQL用戶,最好的轉義函數是 MySQL_real_escape_string( )。如果你使用的數據庫沒有PHP內建轉義函數可用的話,addslashes( )是最後的選擇。
       下面的例子說明了對於MySQL數據庫的正確的轉義技巧:
CODE:

<?PHP
  $MySQL = array(  );
  $mysql['username'] = MySQL_real_escape_string($clean['username']);
  $sql = "SELECT *
          FROM   profile
          WHERE  username = '{$MySQL['username']}'";
  $result = MySQL_query($sql);
  ?>

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved