Session 是 Web 開發中必須涉及的一個話題,面試的時候很多人理解 Session 和 Cookie 的時候總是就一句“一個存儲在服務器端,一個存儲客戶端”,被面試的人回答的時候可能自己也覺得很空洞,而面試官肯定也會很不滿意,其實完全可以換個話題來考察對於 Session 的理解,這就是這篇博文的題目“正確理解 Session 的安全性”。
在 PHP 語言中,Session 安全性這個鍋不能全部讓 PHP 背。Session 不安全的根本原因其實就是二點,第一個就是會話傳遞機制導致的(Cookie 和 URL參數)劫持問題(只要有網絡就會有劫持,所以劫持不是這篇博文的重點),另外個就是會話固定的問題。
假如你僅僅使用 PHP 原生的函數去管理你的 Session,可能會有安全性的問題,所以提倡使用第三方的 Session 類庫(它考慮很多安全性問題)。
另外 PHP 本身也提供了很多指令和方法來提高安全性(很多文章描述 Session 有多么不安全,並且弄出了很多解決方案,但其實高版本的 PHP 提供了很多內置的解決方案,這就是看手冊的重要性)。
簡單描述下 Session ,Session 的數據是存儲在服務器端的,那么服務器如何保持會話呢(或者說如何找到特定的用戶呢?),可以通過二種方式(Cookie 和 URL 參數)來傳遞。一涉及到傳遞就比如會有危險,並且在 PHP 中這個危險被放大了。
僅僅使用 Cookie 來存放 Session ID
Session ID 可以使用 Cookie 和 URL 參數來傳遞,相對來說,Cookie 更安全( URL 暴露的頻率更高),假如你不考慮 Cookie 被禁用的問題,你應該只使用 Cookie 來傳遞。PHP.ini 提供了 session.use_only_cookies 指令來加強安全性。
保護你的 Cookie
Cookie 本身也是不安全的,所以提升 Cookie 安全性的方法也同樣適用與 Session。
關鍵的就是 HttpOnly(不允許 Javascript 腳本讀取),另外假如你的網站是支持 Https 協議的,那么配置 Cookie 只能通過 Https 協議傳遞。
Cookie 和 Session 的過期時間保持一致。
很多開發者非常注意 Session 的 GC 過期時間,GC 代表到了特定日期,PHP 會自動清除服務器端的 Session 文件,但是還很多人理解這個概念有誤區:
Session 的存活時間是“變化的”,只要會話一初始化,Session 文件的修改時間就會更新,換句話說,設置該 GC 時間為 2 小時過期,但是只要用戶間隔 2 小時一直保持會話更新(比如刷新頁面),則 Session 一直會存在。
GC 到期后,由 PHP 來控制刪除 Session 文件,但是並不是每次就會刪除過期文件,所以不能依賴它(但是可以通過自定義 Session 管理器來實現這個機制)。
考慮到 Session GC 不可依賴,所以間接的可以設置 Cookie 對應的過期時間(session.cookie_lifetime 指令)等於 GC 時間,這樣一到過期時間,由於 Cookie 失效了,等同於 Session 也失效了(雖然 Session 對應的文件並沒有刪除,假如攻擊者知道 Session ID 還是會帶來安全問題)。
Session Time-Outs
由於 Session 的 GC 不能依賴,所以很多類庫通過一些方案來解決該問題,考慮這樣一個場景“假如你登陸了一個網站,並且很長時間沒有操作了”,那么這個會話有效嗎?理論上來說應該不算有效,會話的有效時間應該是“變化的”,當用戶觸發了會話,則更新一次(等於將過期時間延長)。
session_start();
$activeTime = 3600;
$t = time();
$lastactivetime = $_SESSION["lastactivetime"] ;
if (isset($lastactivetime)) {
if (($lastactivetime + $activeTime) > $t) {
return;
}
}
$_SESSION["lastactivetime"] = $t;
在 PHP CodeIgniter 框架中,這個激活有效時間稱為 sess_time_to_update。
當然這個方法也並不是特別安全,因為一般來說攻擊者會不斷的維持會話處於“激活狀態”。
Regenerate the Session ID
假如攻擊者劫持了你的 Session ID,但是你又不知道,為了安全建議經常性的重置 Session ID(比如不定期的使用session_regenerate_id()函數),這樣攻擊者等同於拿到的是舊的 Session ID(假設新的 Session ID 攻擊者獲取不到),這樣就不能獲取 Session 數據了。
但這也不是完全有效的(因為攻擊者有方法能劫持你的 Session ID)。
更有效的方式其實應該是在用戶操作更高權限功能的時候(比如電商結賬的時候),讓用戶重新輸入密碼去驗證(獲取用戶密碼是另外一種攻擊方式 )。這是從應用層角度考慮最有效的保護方式。
會話固定
會話固定我理解為二層意思,只要有正確的 Session ID ,不管你從什么設備上發起請求,服務器程序校驗出有對應的 Session 信息就認為是正確的,第二層意思就是假如客戶端偽造一個 Session ID(偽造包含二部分,第一本身服務器上沒有這個 Session ID,第二 Session ID的值格式也可以是錯誤的),PHP 處理的時候看到這個 Session ID不存在,就以這個Session ID 去初始化,這樣的機制可能帶來攻擊。
舉個例子,一個攻擊者發送你一個連接,比如 https://www.jd.com/?SID=1234,你打開后,看到的是京東的網站,然后你登錄了(這時候有了正確的身份驗證信息),攻擊者這時候在自己的電腦就能以你的身份購買東西了(因為攻擊者和用戶是同一個 Session ID)。
那么如何解決呢?除了上面提到的一系列方法,還可以提供二個方法。
第一種是應用層的解決方案,上面說了 Session ID 和客戶端環境沒有關系,那么可以加上這個關系,比如初始化 Session ID 的時候,可以將用戶瀏覽器的 UA 頭保存到 Session 信息中,假如攻擊者想通過這個 Session ID 獲取權限的時候,服務器端代碼一看這個 Session ID 中的 UA 信息和訪問者不一樣,就認為是非法請求。
第二種解決方案是 PHP 提供的,session.use_strict_mode 指令假如開啟,未初始化的 Session ID PHP 會重新生成一個,很大程度上解決了安全問題。
在這個指令沒出現的情況下,一般通過如下方式去解決:
//使用 session_id 作為校驗 ID
session_destory();
session_regenerate_id();
$_SESSION['valid_id'] = session_id();
//校驗 session_id 是否初始化
if ($_SESSION['valid_id'] !== session_id()) {
session_regenerate_id();
$_SESSION['valid_id'] = session_id();
}
最后,安全問題是相對的,沒有絕對的安全,盡量讓其更安全,並通過應用解決方案去提升安全。
並且這里也沒有提到如何進行 Session 劫持的問題,這已經不屬於 PHP 解決的范疇了。
正確的關閉會話方式
很多開發者在用戶退出的時候,不會主動或者正確的關閉會話,關閉會話包含三個方面,第一就是傳遞 Session ID 的 Cookie 應該刪除,第二就是 Session 文件應該刪除,第三在 PHP 進程中的 Session 全局變量也應該清除,用代碼來說明下:
$_SESSION = array();
session_unset();
$name = session_name();
if (isset($_COOKIE[$name])) {
$r = session_get_cookie_params();
setcookie($name, '', time() - 3600, $r['path'], $r['domain'], $r['
secure'], $r['httponly']);
}
session_destroy();