有一點咱們必須認可,大多數web應用程序都離不開session的使用。這篇文章將會結合php以及http協議來分析如何創建一個安全的會話管理機制。咱們先簡單的瞭解一些http的知識,從而理解該協議的無狀態特性。而後,學習一些關於cookie的基本操做。最後,我會一步步闡述如何使用一些簡單,高效的方法來提升你的php應用程序的安全性以及穩定行。程序員
我想大多數的php初級程序員必定會認爲php默認的session機制的安全性彷佛是有必定保障的,事實剛好相反 – php團隊只是提供了一套便捷的session的解決方案提供給程序員使用,至於安全性的話,應該由程序員來增強,這是應用程序開發團隊的責任。由於,這裏面的方法不少,能夠這麼說吧,沒有最好,只有更好。攻擊的方式在不斷變化,防守方也須要不斷變招,因此,我我的認爲php團隊的作法仍是比較明智的。web
無狀態性算法
Http是一種無狀態性的協議。這是由於此種協議不要求瀏覽器在每次請求中標明它本身的身份,而且瀏覽器以及服務器之間並無保持一個持久性的鏈接用於多個頁面之間的訪問。當一個用戶訪問一個站點的時候,用戶的瀏覽器發送一個http請求到服務器,服務器返回給瀏覽器一個http響應。其實很簡單的一個概念,客戶端一個請求,服務器端一個回覆,這就是整個基於http協議的通信過程。apache
由於web應用程序是基於http協議進行通信的,而咱們已經講過了http是無狀態的,這就增長了維護web應用程序狀態的難度, 對於開發者來講,是一個不小的挑戰。Cookies是做爲http的一個擴展誕生的,其主要用途是彌補http的無狀態特性,提供了一種保持客戶端與服務器端之間狀態的途徑,可是因爲出於安全性的考慮,有的用戶在瀏覽器中是禁止掉cookie的。這種狀況下,狀態信息只能經過url中的參數來傳遞到服務器端,不過這種方式的安全性不好。事實上,按照一般的想法,應該有客戶端來代表本身的身份,從而和服務器之間維持一種狀態,可是出於安全性方面的考慮,咱們都應該明白一點 – 來自客戶端的信息都是不能徹底信任的。數組
儘管這樣,針對維持web應用程序狀態的問題,相對來講,仍是有比較優雅的解決方案的。不過,應該說是沒有完美的解決方案的,再好的解決方案也不可能適用全部的狀況。這篇文章將介紹一些技術。這些技術能夠用來比較穩定地維持應用程序的狀態以及抵禦一些針對session的攻擊,好比會話劫持。而且你能夠學習到cookie是怎樣工做的,php 的session作了那些事情,以及怎樣才能劫持session。瀏覽器
HTTP 概覽緩存
如何才能保持web應用程序的狀態以及選擇最合適的解決方案呢?在回答這個問題以前,必須得先了解web的底層協議 – Hypertext Transfer Protocol (HTTP)。安全
當用戶訪問http://example.com這個域名的時候,瀏覽器就會自動和服務器創建tcp/ip鏈接,而後發送http請求到example.com的服務器的80端口。該個請求的語法以下所示:
GET / HTTP/1.1
Host: example.org
以上第一行叫作請求行,第二個參數(一個反斜線在這個例子中)表示所請求資源的路徑。反斜線表明了根目錄;服務器會轉換這個根目錄爲服務器文件系統中的一個具體目錄。
Apache的用戶經常使用DocumentRoot這個命令來設置這個文檔根路徑。若是請求的url是http://example.org/path/to/script.php,那麼請求的路徑就是/path/to/script.php。假如document root 被定義爲usr/lcoal/apache/htdocs的話,整個請求的資源路徑就是/usr/local/apache/htdocs/path/to/script.php。
第二行描述的是http頭部的語法。在這個例子中的頭部是Host, 它標識了瀏覽器但願獲取資源的域名主機。還有不少其它的請求頭部能夠包含在http請求中,好比user-Agent頭部,在php能夠經過$_SERVER['HTTP_USER_AGENT']獲取請求中所攜帶的這個頭部信息。
可是遺憾的是,在這個請求例子中,沒有任何信息能夠惟一標識當前這個發出請求的客戶端。有些開發者藉助請求中的ip頭部來惟一標識發出這次請求的客戶端,可是這種方式存在不少問題。由於,有些用戶是經過代理來訪問的,好比用戶A經過代理B鏈接網站www.example.com, 服務器端獲取的ip信息是代理B分配給A的ip地址,若是用戶這時斷開代理,而後再次鏈接代理的話,它的代理ip地址又再次改變,也就說一個用戶對應了多個ip地址,這種狀況下,服務器端根據ip地址來標識用戶的話,會認爲請求是來自不一樣的用戶,事實上是同一個用戶。 還用另一種狀況就是,好比不少用戶是在同一個局域網裏經過路由鏈接互聯網,而後都訪問www.example.com的話,因爲這些用戶共享同一個外網ip地址,這會致使服務器認爲這些用戶是同一個用戶發出的請求,由於他們是來自同一個ip地址的訪問。
保持應用程序狀態的第一步就是要知道如何來惟一地標識每一個客戶端。由於只有在http中請求中攜帶的信息才能用來標識客戶端,因此在請求中必須包含某種能夠用來標識客戶端惟一身份的信息。Cookie設計出來就是用來解決這一問題的。
Cookies
若是你把Cookies當作爲http協議的一個擴展的話,理解起來就容易的多了,其實本質上cookies就是http的一個擴展。有兩個http頭部是專門負責設置以及發送cookie的,它們分別是Set-Cookie以及Cookie。當服務器返回給客戶端一個http響應信息時,其中若是包含Set-Cookie這個頭部時,意思就是指示客戶端創建一個cookie,而且在後續的http請求中自動發送這個cookie到服務器端,直到這個cookie過時。若是cookie的生存時間是整個會話期間的話,那麼瀏覽器會將cookie保存在內存中,瀏覽器關閉時就會自動清除這個cookie。另一種狀況就是保存在客戶端的硬盤中,瀏覽器關閉的話,該cookie也不會被清除,下次打開瀏覽器訪問對應網站時,這個cookie就會自動再次發送到服務器端。一個cookie的設置以及發送過程分爲如下四步:
客戶端發送一個http請求到服務器端
服務器端發送一個http響應到客戶端,其中包含Set-Cookie頭部
客戶端發送一個http請求到服務器端,其中包含Cookie頭部
服務器端發送一個http響應到客戶端
這個通信過程也能夠用如下下示意圖來描述:
在客戶端的第二次請求中包含的Cookie頭部中,提供給了服務器端能夠用來惟一標識客戶端身份的信息。這時,服務器端也就能夠判斷客戶端是否啓用了cookies。儘管,用戶可能在和應用程序交互的過程當中忽然禁用cookies的使用,可是,這個狀況基本是不太可能發生的,因此能夠不加以考慮,這在實踐中也被證實是對的。
GET and POST Data
除了cookies,客戶端還能夠將發送給服務器的數據包含在請求的url中,好比請求的參數或者請求的路徑中。 咱們來看一個例子:
GET /index.php?foo=bar HTTP/1.1
Host: example.org
以上就是一個常規的http get 請求,該get請求發送到example.org域名對應的web 服務器下的index.php腳本, 在index.php腳本中,能夠經過$_GET['foo']來獲取對應的url中foo參數的值,也就是’bar’。大多數php開發者都稱這樣的數據會GET數據,也有少數稱它爲查詢數據或者url變量。可是你們須要注意一點,不是說GET數據就只能包含在HTTP GET類型的請求中,在HTTP POST類型的請求中一樣能夠包含GET數據,只要將相關GET數據包含在請求的url中便可,也就是說GET數據的傳遞不依賴與具體請求的類型。
另一種客戶端傳遞數據到服務器端的方式是將數據包含在http請求的內容區域內。 這種方式須要請求的類型是POST的,看下面一個例子:
POST /index.php HTTP/1.1
Host: example.org
Content-Type: application/x-www-form-urlencoded
Content-Length: 7
foo=bar
在這種狀況下,在腳本index.php能夠經過調用$_POST['foo']來獲取對應的值bar。開發者稱這個數據爲POST數據,也就是你們熟知的form以post方式提交請求的方式。
在一個請求中,能夠同時包含這兩種形式的數據:
POST /index.php?myget=foo HTTP/1.1
Host: example.orgContent-Type: application/x-www-form-urlencoded
Content-Length: 11
mypost=bar
這兩種傳遞數據的方式,比起用cookies來傳遞數據更穩定,由於cookie可能被禁用,可是以GET以及POST方式傳遞數據時,不存在這種狀況。咱們能夠將PHPSESSID包含在http請求的url中,就像下面的例子同樣:
GET /index.php?PHPSESSID=12345 HTTP/1.1
Host: example.org
以這種方式傳遞session id的話,能夠跟用cookie頭部傳遞session id同樣,達到一樣的效果, 可是,缺點就是須要開發者認爲地將session id附加在url中或者做爲隱藏字段加入到表單中。不像cookie同樣,只要服務器端指示客戶端建立cookie成功之後,客戶端在後續的請求中,會自動第將對應的沒有過時的cookie傳遞給服務器端。固然,php在開啓session.use_trans_sid後,也能夠自動地將session id 附加在url中以及表單的隱藏字段中,可是這個選項不建議開啓,由於存在安全問題。這樣的話,容易泄露session id, 好比有的用戶會bookmark一個url或者分享一個url,那麼session id也就暴露了,加入這個session id尚未過時,那是有必定的安全問題存在的,除非服務器端,除了session id外,還附加了其它方式進行驗證用戶的合法性!
儘管以POST的方式來傳遞session id的話,相對GET的方式來講,會安全的多。可是,這種方式的缺點就是比較麻煩,由於這樣的話,在你的應用程序中比較將全部的請求都轉換成post的請求,這顯然是不太合適的。
Session的管理
直到如今,我只討論瞭如何維護應用程序的狀態,只是簡單地涉及到了若是保持請求之間的關係。接下來,我闡述下在實際中用到比較多的技術 – Session的管理。涉及到session的管理,就不是單單地維持各個請求之間的狀態,還須要維持會話期間針對每一個特定用戶使用到的數據。咱們經常把這種數據叫作session數據,由於這些數據是跟某個特定用戶與服務器之間的會話相關聯的。若是你使用php內置的session的管理機制,那麼session數據通常是保存在/tmp這個服務器端的文件夾中,而且其中的session數據會被自動地保存到超級數組$_SESSION中。一個最簡單的使用session的例子,就是將相關的session數據從一個頁面傳遞(注意:實際傳遞的是session id)到另外一個頁面。下面用示例代碼1, start.php, 對這個例子加以演示:
- <?php
- session_start();
- $_SESSION['foo'] = 'bar';
- ?>
- <a href="continue.php">continue.php</a>
假如用戶點擊start.php中的連接訪問continue.php,那麼在continue.php中就能夠經過$_SESSION['foo']獲取在start.php中的定義的值’bar’。看下面的示例代碼2:
示例代碼2 – continue.php
- <?php
- session_start();
- echo $_SESSION['foo']; /* bar */
- ?>
是否是很是簡單,可是我要指出的話,若是你真的這樣來寫代碼的話,說明你對php底層的對於session的實現機制還不是很是瞭解透徹。在不瞭解php內部給你自動作了多少事情的狀況下,你會發現若是程序出錯的話,這樣的代碼將變的很難調試,事實上,這樣的代碼也徹底沒有安全性可言。
Session的安全性問題
一直以來不少開發者都認爲php內置的session管理機制是具備必定的安全性,能夠對通常的session攻擊起到防護。事實上,這是一種誤解,php團隊只實現了一種方便有效的機制。具體的安全措施,應該有應用程序的開發團隊來實施。 就像開篇談到的,沒有最好的解決方案,只有最合適你的方案。
如今,咱們來看下一個比較常規的針對session的攻擊:
用戶訪問http://www.example.org,而且登陸。
example.org的服務器設置指示客戶端設置相關cookie – PHPSESSID=12345
攻擊者這時訪問http://www.example.org/,而且在請求中攜帶了對應的cookie – PHPSESSID=12345
這樣狀況下,由於example.orge的服務器經過PHPSESSID來辨認對應的用戶的,因此服務器錯把攻擊者當成了合法的用戶。
整個過程的描述,請看下面的示例圖:
固然這種攻擊的方式,前提條件是攻擊者必須經過某種手段固定,劫持或者猜想出某個合法用戶的PHPSESSID。雖然這看起來難度很高,可是也不是不可能的事情。
安全性的增強
有不少技術能夠用來增強Session的安全性,主要思想就是要使驗證的過程對於合法用戶來講,越簡單越好,而後對於攻擊者來講,步驟要越複雜越好。固然,這彷佛是比較難於平衡的,要根據你應用程序的具體設計來作決策。
最簡單的居於HTTP/1.1請求包括請求行以及一些Host的頭部:
GET / HTTP/1.1
Host: example.org
若是客戶端經過PHPSESSID傳遞相關的session標識符,能夠將PHPSESSID放在cookie頭部中進行傳遞:
GET / HTTP/1.1
Host: example.org
Cookie: PHPSESSID=12345
一樣地,客戶端也能夠將session標識符放在請求的url中進行傳遞。
GET /?PHPSESSID=12345
HTTP/1.1Host: example.org
固然,session標識符也能夠包含在POST數據中,可是這對用戶體驗有影響,因此這種方式不多采用。
由於來自TCP/IP信息也不必定能夠徹底信任的,因此,對於web開發者來講,利用TCP/IP中的信息來增強安全性也是不太合適的。 不過,攻擊者也必須提供一個合法用戶的惟一的標識符,才能假扮成合法用戶進入系統。所以,看起來惟一可以有效的保護系統的措施,就是儘可能地隱藏session標識符或者使之難於猜想出來。最好就是二者都能實施。
PHP會自動生成一個隨機的session ID,基原本說是不可能被猜想出來的,因此這方面的安全仍是有必定保障的。可是,要防止攻擊者獲取一個合法的session ID是至關困難的,這基本上不是開發者所能控制的。
事實上,許多狀況下都有可能致使session ID的泄露。 好比說,若是經過GET數據來傳遞session ID的話,就有可能暴露這個敏感的身份信息。由於,有的用戶可能會將帶有session ID的連接緩存,收藏或者發送在郵件內容中。Cookies是一種像相對來講安全一點的機制,可是用戶是能夠在客戶端中禁止掉cookies的!在一些IE的版本中也有比較嚴重的安全漏洞,比較有名的就是會泄露cookies給一些有安全隱患的邪惡站點。
所以,做爲一個開發者,能夠確定session ID是不能被猜想出來的,可是仍是有可能被攻擊者使用某些方法獲取到。因此,必須採起一些額外的安全措施來防止此類狀況在你的應用程序中發生。
實際上,一個標準的HTTP請求中除了Host等必須包含的頭部,還包含了一些可選的頭部.舉一個例子,看下面的一個請求:
GET / HTTP/1.1
Host: example.org
Cookie: PHPSESSID=12345
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1
Accept: text/html;q=0.9, */*;q=0.1
Accept-Charset: ISO-8859-1, utf-8;q=0.66, *;q=0.66
Accept-Language: en
咱們能夠看到,在以上的一個請求例子中包含了四個額外的頭部,分別是User-Agent, Accept, Accept-Charset以及Accept-Language。由於這些頭部不是必須的,因此徹底依賴他們在你的應用程序中發揮做用是不太明智的。可是,若是一個用戶的瀏覽器確實發送了這些頭部到服務器,那麼能夠確定的是在接下來的同一個用戶經過同一個瀏覽器發送的請求中,必然也會攜帶這些頭部。固然,這其中也會有極少數的特殊狀況發生。假如以上例子是由一個當前的跟服務器創建了會話的用戶發出的請求,考慮下面的一個請求:
GET / HTTP/1.1
Host: example.org
Cookie: PHPSESSID=12345
User-Agent: Mozilla/5.0
由於有相同的session id包含在請求的Cookie頭部中,因此相同的php session將會被訪問到。可是,請求裏的User-Agent頭部跟先前的請求中的信息是不一樣的,系統是否能夠假定這兩個請求是同一個用戶發出的?
像這種狀況下,發現瀏覽器的頭部改變了,可是不能確定這是不是一次來自攻擊者的請求的話,比較好的措施就是彈出一個要求輸入密碼的輸入框讓用戶輸入,這樣的話,對用戶體驗的影響不會很大,又能頗有效地防止攻擊。
固然,你能夠在系統中加入覈查User-Agent頭部的代碼,相似示例3中的代碼:
示例代碼3
- <?php
- session_start();
- if (md5($_SERVER['HTTP_USER_AGENT']) != $_SESSION['HTTP_USER_AGENT'])
- { /* 彈出密碼輸入框 */ exit;
- }
- ?>
固然,你先必須在第一次請求時,初始化session的時候,用MD5算法加密user agent信息而且保存在session中,相似下面示例4中的代碼:
示例代碼4
- <?php
- session_start();
- $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
- ?>
雖然不必定須要用MD5來加密這個User-Agent信息,但使用這種方式之後就不須要再過濾這個$_SERVER['HTTP_USER_AGENT']數據了。否則的話,在使用這個數據之前必需要進行數據過濾,由於任何來自客戶端的數據都是不可信任的,必需要注意這一點。
在你檢查這個User-Agent客戶端頭部信息之後,作爲一個攻擊者必需要完成兩步才能劫持一個session:
獲取一個合法的session id
包含一個相同的User-Agent頭部在僞造的請求中
你可能會說,竟然攻擊者能得到有效的session id,那麼以他的水平,僞造一個相同的User-Agent不是件難事。不錯,可是咱們能夠說這至少給他添加了一些麻煩,在必定程度上也增長了session機制的安全性。
你應該也能想到了,既然咱們能夠檢查User-Agent這個頭部來增強安全性,那麼不妨再利用其它的一些頭部信息,把他們組合起來生成一個加密的token,而且讓客戶端在後續的請求中攜帶這個token!這樣的話,攻擊者基本上不可能猜想出這樣一個token是怎麼生成出來的。這比如你用信用卡在超市付款,一個你必須有信用卡(比如session id),另外你也必須輸入一個支付密碼(比如token),這有這二者都符合的狀況下,你才能成功進入帳號付款。 看下面一段代碼:
- <?php
- session_start();
- $token = 'SHIFLETT' . $_SERVER['HTTP_USER_AGENT'];
- $_SESSION['token'] = md5($token . session_id());
- ?>
- 注意:Accept這個頭部不該該被用來生成token,由於有些瀏覽器會自動改變這個頭部,當用戶刷新瀏覽器的時候。
在你的驗證機制中加入了這個很是難於猜想出來的token之後,安全性會獲得很大的提高。假如這個token經過像session id同樣的方式來進行傳遞,這種狀況下,一個攻擊者必須完成必要的3步來劫持用戶的session:
獲取一個合法的session ID
在請求中加入相同的User-Agent頭部,用與生成token
在請求中攜帶被攻擊者的token
這裏面有個問題。若是session id以及token都是經過GET數據來傳遞的話,那麼對於能獲取session ID的攻擊者,一樣就可以獲取到這個token。因此,比較安全靠譜的方式應該是利用兩種不一樣的數據傳遞方式來分別傳遞session id以及token。例如,經過cookie來傳遞session id,而後經過GET數據來傳遞token。所以,假如攻擊者經過某種手段得到了這個惟一的用戶身份標識,也是不太可能同時輕鬆地獲取到這個token,它相對來講依然是安全的。
還有不少的技術手段能夠用來增強你的session機制的安全性。但願你在大體瞭解session的內部本質之後,能夠設計出適合你的應用系統的驗證機制,從而大大的提升系統的安全性。畢竟,你是最熟悉當下你開發的系統的開發者之一,能夠根據實際狀況來實施一些特有的,額外的安全措施。
總結
以上只是大概地描述了session的工做機制,以及簡單地闡述了一些安全措施。但要記住,以上的方法都是可以增強安全性,不是說可以徹底保護你的系統,但願讀者本身再去調研相關內容。在這個調研過程當中,相信你會學到頗有實際使用價值的方案。
原文地址:http://shiflett.org/articles/the-truth-about-sessions