我們先簡單的瞭解一些http的知識,從而理解該協議的無狀態特性。然後,學習一些關於cookie的基本操作。最後,我會一步步闡述如何使用一些簡單,高效的方法來提高你的php應用程式的安全性以及穩定行。
我想大多數的php初級程式員一定會認為php預設的session機制的安全性似乎是有一定保障的,事實恰好相反 – php團隊只是提供了一套便捷的session的解決方案提供給程式員使用,至於安全性的話,應該由程式員來加強,這是應用程式Team Dev的責任。因為,這裡面的方法很多,可以這麼說吧,沒有最好,只有更好。攻擊的方式在不斷變化,防守方也需要不斷變招,所以,我個人認為php團隊的做法還是比較明智的。
一、HTTP無狀態性
Http是一種無狀態性的協議。這是因為此種協議不要求瀏覽器在每次請求中標明它自己的身份,並且瀏覽器以及伺服器之間並沒有保持一個持久性的串連用於多個頁面之間的訪問。當一個使用者訪問一個網站的時候,使用者的瀏覽器發送一個http請求到伺服器,伺服器返回給瀏覽器一個http響應。其實很簡單的一個概念,用戶端一個請求,伺服器端一個回複,這就是整個基於http協議的通訊過程。
因為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的設定以及發送過程分為以下四步:
1.用戶端發送一個http請求到伺服器端
2.伺服器端發送一個http響應到用戶端,其中包含Set-Cookie頭部
3.用戶端發送一個http請求到伺服器端,其中包含Cookie頭部
4.伺服器端發送一個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網域名稱對應的網頁伺服器下的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
[code]
這兩種傳遞資料的方式,比起用cookies來傳遞資料更穩定,因為cookie可能被禁用,但是以GET以及POST方式傳遞資料時,不存在這種情況。我們可以將PHPSESSID包含在http請求的url中,就像下面的例子一樣:
[code]
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, 對這個例子加以示範:
範例程式碼1 – start.php
複製代碼 代碼如下:
continue.php
假如使用者點擊start.php中的連結訪問continue.php,那麼在continue.php中就可以通過$_SESSION['foo']擷取在start.php中的定義的值'bar'。看下面的範例程式碼2:
範例程式碼2 – continue.php
複製代碼 代碼如下:
是不是非常簡單,但是我要指出的話,如果你真的這樣來寫代碼的話,說明你對php底層的對於session的實現機制還不是非常瞭解透徹。在不瞭解php內部給你自動做了多少事情的情況下,你會發現如果程式出錯的話,這樣的代碼將變的很難調試,事實上,這樣的代碼也完全沒有安全性可言。
六、session的安全性問題
一直以來很多開發人員都認為php內建的session管理機制是具有一定的安全性,可以對一般的session攻擊起到防禦。事實上,這是一種誤解,php團隊只實現了一種方便有效機制。具體的安全措施,應該有應用程式的Team Dev來實施。 就像開篇談到的,沒有最好的解決方案,只有最合適你的方案。
現在,我們來看下一個比較常規的針對session的攻擊:
1..使用者訪問http://www.example.org,並且登入。
2.example.org的伺服器設定指示用戶端設定相關cookie – PHPSESSID=12345
3.攻擊者這時訪問http://www.example.org/,並且在請求中攜帶了對應的cookie – PHPSESSID=12345
4.這樣情況下,因為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:
複製代碼 代碼如下:
當然,你先必須在第一次請求時,初始化session的時候,用MD5演算法加密user agent資訊並且儲存在session中,類似下面樣本4中的代碼:
範例程式碼 4:
複製代碼 代碼如下:
雖然不一定需要用MD5來加密這個User-Agent資訊,但使用這種方式以後就不需要再過濾這個$_SERVER['HTTP_USER_AGENT']資料了。不然的話,在使用這個資料以前必須要進行資料過濾,因為任何來自用戶端的資料都是不可信任的,必須要注意這一點。
在你檢查這個User-Agent用戶端頭部資訊以後,做為一個攻擊者必須要完成兩步才能劫持一個session:
1.擷取一個合法的session id
2.包含一個相同的User-Agent頭部在偽造的請求中
你可能會說,居然攻擊者能獲得有效session id,那麼以他的水平,偽造一個相同的User-Agent不是件難事。不錯,但是我們可以說這至少給他添加了一些麻煩,在一定程度上也增加了session機制的安全性。
你應該也能想到了,既然我們可以檢查User-Agent這個頭部來加強安全性,那麼不妨再利用其它的一些頭部資訊,把他們組合起來產生一個加密的token,並且讓用戶端在後續的請求中攜帶這個token!這樣的話,攻擊者基本上不可能猜測出這樣一個token是怎麼產生出來的。這好比你用信用卡在超市付款,一個你必須有信用卡(好比session id),另外你也必須輸入一個支付密碼(好比token),這有這兩者都符合的情況下,你才能成功進入帳號付款。 看下面一段代碼:
複製代碼 代碼如下:
注意:Accept這個頭部不應該被用來產生token,因為有些瀏覽器會自動改變這個頭部,當使用者重新整理瀏覽器的時候。
在你的驗證機制中加入了這個非常難於猜測出來的token以後,安全性會得到很大的提升。假如這個token通過像session id一樣的方式來進行傳遞,這種情況下,一個攻擊者必須完成必要的3步來劫持使用者的session:
1.擷取一個合法的session ID
2.在請求中加入相同的User-Agent頭部,用與產生token
3.在請求中攜帶被攻擊者的token
4.這裡面有個問題。如果session id以及token都是通過GET資料來傳遞的話,那麼對於能擷取session ID的攻擊者,同樣就能夠擷取到這個token。所以,比較安全靠譜的方式應該是利用兩種不同的資料傳遞方式來分別傳遞session id以及token。例如,通過cookie來傳遞session id,然後通過GET資料來傳遞token。因此,假如攻擊者通過某種手段獲得了這個唯一的使用者身份標識,也是不太可能同時輕鬆地擷取到這個token,它相對來說依然是安全的。
還有很多的技術手段可以用來加強你的session機制的安全性。希望你在大致瞭解session的內部本質以後,可以設計出適合你的應用系統的驗證機制,從而大大的提高系統的安全性。畢竟,你是最熟悉當下你開發的系統的開發人員之一,可以根據實際情況來實施一些特有的,額外的安全措施。
八、總結
以上只是大概地描述了session的工作機制,以及簡單地闡述了一些安全措施。但要記住,以上的方法都是能夠加強安全性,不是說能夠完全保護你的系統,希望讀者自己再去調研相關內容。在這個調研過程中,相信你會學到很有實際使用價值的方案。
http://www.bkjia.com/PHPjc/751932.htmlwww.bkjia.comtruehttp://www.bkjia.com/PHPjc/751932.htmlTechArticle我們先簡單的瞭解一些http的知識,從而理解該協議的無狀態特性。然後,學習一些關於cookie的基本操作。最後,我會一步步闡述如何使用一...