會話管理基礎
會話安全
會話模組無法保證你儲存在會話中的資訊只能被建立會話的使用者本人可見。 你需要採取額外的手段來保護會話中的機密資訊, 至於採取何種方式來保護機密資訊, 取決於你在會話中儲存的數據的機密程度。
評估會話中儲存的數據的重要性,
以及為此增加額外的保護機制,
通常需要付出一定的代價,同時會降低便利性。
例如,如果你需要保護使用者免受社會工程學攻擊,
你需要啟用 session.use_only_cookies
選項。
這就要求使用者在使用過程中,必須把瀏覽器設定為接受 cookie,
否則就無法正常使用會話功能了。
有很多種方式都可以導致會話 ID 被泄露給第三方。 例如,JavaScript 注入,URL 中包含會話 ID,數據包偵聽, 或者直接訪問你的物理裝置等。 如果會話 ID 被泄漏給第三方, 那麼他們就可以訪問這個會話 ID 可以訪問的全部資源。 首先,如果在 URL 中包含了會話 ID, 並且訪問了外部的站點, 那麼你的會話 ID 可能在外部站點的訪問日誌中被記錄(referrer 請求頭)。 另外,攻擊者也可以監聽你的網路通訊,如果通訊未加密, 那麼會話 ID 將會在網路中以明文的形式進行傳輸。 針對這種情況的解決方案就是在服務端配置 SSL/TLS, 另外,使用 HSTS 可以達到更高的安全性。
注意: 即使使用 HTTPS 協議,也無法百分百保證機密數據不被泄漏。 例如,CRIME 和 BEAST 漏洞可以使得攻擊者讀取到你的數據。 另外,出於網路通訊審計目的,很多網路中都存在 HTTPS MITM 代理, 可以讀取 HTTPS 協議下的通訊數據。 那麼攻擊者也可以搭建類似的代理伺服器,用來竊取 HTTPS 協議下的通訊數據。
嚴格會話管理
目前,預設情況下,PHP 是以自適應的方式來管理會話的, 這種方式使用起來很靈活,但是同樣也帶來了一定的風險。
新增加了一個配置項: session.use_strict_mode。 當啟用這個配置項,並且你所用的會話儲存處理器支援的話,未經初始化的會話 ID 會被拒絕, 併爲其產生一個全新的會話,這可以避免攻擊者使用一個已知的會話 ID 來進行攻擊。 例如,攻擊者可以通過郵件給受害者發送一個包含會話 ID 的鏈接: http://example.com/page.php?PHPSESSID=123456789。 如果啟用了 session.use_trans_sid 配置項, 那麼受害者將會使用攻擊者所提供的會話 ID 開始一個新的會話。 如果啟用了 session.use_strict_mode 選項,就可以降低風險。
使用者自定義的會話儲存器也可以通過實現會話 ID 驗證來支援嚴格會話模式。 建議使用者在實現自己的會話儲存器的時候, 一定要對會話 ID 的合法性進行驗證。
在瀏覽器一側,可以為用來儲存會話 ID 的 cookie 設定域,路徑, 僅允許 HTTP 訪問,必須使用 HTTPS 訪問等安全屬性。 如果使用的是 PHP 7.3. 版本,還可以對 cookie 設定 SameSite 屬性。 攻擊者可以利用瀏覽器的這些特性來設定永久可用的會話 ID。 僅僅設定 session.use_only_cookies 配置項 無法解決這個問題。而 session.use_strict_mode 配置項 可以降低這種風險。設定 session.use_strict_mode=On, 來拒絕未經初始化的會話 ID。
注意: 雖然使用 session.use_strict_mode 配置項 可以降低靈活會話管理方式所帶來的風險, 攻擊者還是通過利用 JavaScript 注入等手段, 強制使用者使用由攻擊者建立的並且經過了正常的初始化的會話 ID。 如何降低這種風向,可以參考本手冊的建議部分。 如果你已經啟用了 session.use_strict_mode 配置項, 同時使用基於時間戳的會話管理, 並且通過設定 session_regenerate_id() 配置項 來重新產生會話 ID, 那麼,攻擊者產生的會話 ID 就可以被刪除掉了。 當發生對過期會話訪問的時候, 你應該儲存活躍會話的所有數據, 以備後續分析使用。 然後讓使用者退出目前的會話,並且重新登錄。 防止攻擊者繼續使用「偷」來的會話。
對過期會話數據的訪問並不總是意味著正在遭受攻擊。 不穩定的網路狀況,或者不正確的會話刪除行為, 都會導致合法的使用者產生訪問過期會話數據的情況。
從 PHP 7.1.0 開始,增加了 session_create_id() 函式。 這個函式允許開發者在會話 ID 中增加使用者 ID 作為字首, 以確保使用者訪問到正確對應的會話數據。 要使用這個函式, 請確保啟用了 session.use_strict_mode 配置項, 否則惡意使用者可能會偽造其他使用者的會話 ID。
注意: 對於 PHP 7.1.0 之前的使用者,應該使用 CSPRNG(例如 /dev/urandom) 或者 random_bytes() 函式以及雜湊函式 來產生新的會話 ID。 session_create_id() 函式本身包含碰撞檢測的能力, 並且根據 INI 檔案中和會話相關的配置項來產生會話 ID。 所以,建議使用 session_create_id() 函式來產生會話 ID。
重新產生會話 ID
雖然 session.use_strict_mode 配置項可以降低風險,但是還不夠。爲了確保會話安全,開發者還需要使用 session_regenerate_id() 函式。
會話 ID 重生機制可以有效的降低會話被竊取的風險, 所以,必須週期性的呼叫 session_regenerate_id() 函式 來重新產生會話 ID, 例如,對於機密內容,每隔 15 分鐘就重新產生會話 ID。 這樣一來,即使會話 ID 被竊取, 那麼攻擊者所得到的會話 ID 也會很快的過期, 如果他們進一步訪問,就會產生對過期會話數據訪問的錯誤。
當用戶成功通過認證之後,必須為其重新產生會話 ID。 並且,必須在向 $_SESSION 中儲存使用者認證資訊之前 呼叫 session_regenerate_id() 函式( session_regenerate_id() 函式 會自動將重生之前的會話數據儲存到新產生的會話)。 請確保只有新的會話包含使用者認證資訊。
開發者不可過分依賴 session.gc_maxlifetime 配置項。 因為攻擊者可以在受害者的會話過期之前訪問系統, 並且維持這個會話的活動,以保證這個會話不會過期。
實際上,你需要自己實現基於時間戳的會話數據管理機制。
雖然會話管理器可以透明的管理時間戳,但是這個特性尚未完整的實現。
在 GC 發生之前,舊的會話數據還得儲存,
同時,開發者還得保證過期的會話數據已經被移除。
但是,開發者又不能立即移除活躍會話中的數據。
所以,不要同時在活躍會話上呼叫 session_regenerate_id(true);
和 session_destroy() 函式。
這聽起來有點兒自相矛盾,但是事實上必須得這麼做。
預設情況下,session_regenerate_id() 函式 不會刪除舊的會話, 所以即使重生了會話 ID,舊的會話可能還是可用的。 開發者需要使用時間戳等機制, 來確保舊的會話數據不會再次被訪問。
刪除活躍會話可能會帶來非預期的一些影響。 例如,在網路狀態不穩定,或者有併發請求到達 Web 伺服器的情況下, 立即刪除活躍會話可能導致個別請求會話失效的問題。
立即刪除活躍會話也無法檢測可能存在的惡意訪問。
作為替代方案, 你要在 $_SESSION 中設定一個很短的過期時間, 然後根據這個時間戳來判斷後續的訪問是被允許的還是被禁止的。
在呼叫 session_regenerate_id() 函式之後, 不能立即禁止對舊的會話數據的訪問,應該再一小段之間之後再禁止訪問。 例如,在穩定的網路條件下,可以設定為幾秒鐘, 在不穩定的網路條件下,可以設定為幾分鐘。
如果使用者訪問了舊的會話數據(已經過期的), 那麼應該禁止訪問。 建議從會話中移除這個使用者的認證資訊,因為這看起來像是在遭受攻擊。
如果攻擊者設定了不可刪除的 cookie,那麼使用 session.use_only_cookies 和 session_regenerate_id() 會導致正常使用者遭受拒絕服務的問題。 如果發生這種情況,請讓使用者刪除 cookie 並且警告使用者他可能面臨一些安全問題。 攻擊者可以通過惡意的 Web 應用、瀏覽器外掛以及對安全性較差的物理裝置進行攻擊 來偽造惡意的 cookie。
請勿誤解這裡的拒絕服務攻擊風險所指的含義。
通常來講,要保護會話 ID 的安全,use_strict_mode=On
是必須要做的。
建議所有的站點都啟用 use_strict_mode=On
。
只有當賬號處於被攻擊的時候,才會發生拒絕服務的問題。 通常都是由於應用中被注入了惡意的 JavaScript 才會導致這個問題。
會話中數據的刪除
過期的會話中的數據應該是被刪除的,並且不可訪問。 現在的會話模組尚未很好的支援這種特性。
應該儘可能快的刪除過期會話中的數據。 但是,活躍會話一定不要立即刪除。 爲了能夠同時滿足這兩點要求, 你需要自己來實現基於時間戳的會話數據管理機制。
在 $_SESSION 中設定會話過期時間戳,並且對其進行管理, 以便能夠阻止對於過期會話的訪問。 當發生對於過期會話的訪問時,建議從相關使用者的所有會話中刪除認證資訊, 並且要求使用者重新認證。 對於過期會話數據的訪問可能是一種攻擊行為, 爲了保護會話數據,你需要追蹤每個使用者的活躍會話。
注意: 當用戶處於不穩定的網路,或者 web 應用存在併發的請求的時候, 也可能發生對於過期會話數據的訪問。 伺服器嘗試為使用者設定新的會話 ID, 但是很可能由於網路原因,導致 Set-Cookie 的數據包無法到達使用者的瀏覽器。 當通過 session_regenerate_id() 函式 為一個連線產生新的會話 ID 之後,其他的併發連線可能尚未得到這個新的會話 ID。 因此,不能立即阻止對於過期會話數據的訪問,而是要延遲一個很小的時間段, 這就是為什麼我們需要實現基於時間戳的會話管理。
簡而言之,不要在呼叫 session_regenerate_id() 或者 session_destroy() 函式的時候立即刪除舊的會話數據, 而是要通過一個時間戳來控制後續對於這個舊會話數據的訪問。 從會話儲存中刪除數據的工作交給 session_gc() 函式來完成吧。
會話和鎖定
預設情況下,爲了保證會話數據在多個請求之間的一致性, 對於會話數據的訪問是加鎖進行的。
但是,這種鎖定機制也會導致被攻擊者利用,來進行對於使用者的拒絕服務攻擊。
爲了降低這種風險,請在訪問會話數據的時候,儘可能的縮短鎖定的時間。
當某個請求不需要更新會話數據的時候,使用只讀模式訪問會話數據。
也就是說,在呼叫 session_start() 函式的時候,
使用 'read_and_close' 選項:session_start(['read_and_close'=>1]);
。
另外,如果需要更新會話數據,那麼在更新完畢之後,
馬上呼叫 session_commit() 函式來釋放對於會話數據的鎖。
當會話不活躍的時候,目前的會話模組不會檢測對於 $_SESSION 的修改。 你需要自己來保證 在會話處於不活躍狀態的時候,不要去修改它。
活躍會話
開發者需要自己來追蹤每個使用者的活躍會話, 要知道每個使用者建立了多少活躍會話,每個活躍會話來自那個 IP 地址, 活躍了多長時間等。 PHP 不會自動完成這項工作,需要開發者來完成。
有很多種方式可以做到追蹤使用者的活躍會話。 你可以通過在數據庫中儲存會話資訊來跟蹤使用者會話。 由於會話是可以被垃圾收集器收集掉的, 所以你也需要處理被收集掉的會話數據, 以保證數據庫中的數據和真實的活躍會話數據的一致性。
一種很簡單的方式就是使用「使用使用者 ID 作為會話 ID 字首」,並且儲存必要的資訊到 $_SESSION 中。 大部分的數據庫產品對於字串字首查詢(譯註:也即右模糊查詢,可以利用索引)都有很好的效能表現。 爲了實現這種方式,可以使用 session_regenerate_id() 和 session_create_id() 函式。
永遠不要使用機密數據作為會話 ID 字首! 如果使用者 ID 屬於機密數據,那麼可以考慮使用 hash_hmac() 函式對其進行摘要后再使用。
必須啟用 session.use_strict_mode 配置項。 請確保已經啟用, 否則活躍會話數據庫可能會被入侵。
要能夠檢測對於過期會話數據的訪問, 基於時間戳的會話數據管理機制是必不可少的。 當檢測到對於過期會話數據的訪問時,你應該從相關使用者的活躍會話中刪除認證資訊, 避免攻擊者持續使用盜取的會話。
會話和自動登錄
開發者不應該通過使用長生命週期的會話 ID 來實現自動登錄功能, 因為這種方式提高了會話被竊取的風險。 開發者應該自己實現自動登錄的機制。
在使用 setcookie() 的時候,傳入安全的一次性摘要結果作為自動登錄資訊。 建議使用比 SHA-2 更高強度的摘要演算法(例如 SHA-256) 對 random_bytes() 隨機產生的數據 (也可以讀取 /dev/urandom 裝置)進行摘要作為自動登錄的資訊。
在使用者訪問的時候,如果發現使用者尚未認證, 那麼就去檢查請求中是否包含了有效的一次性登錄資訊。 如果包含有效的一次性登錄資訊,那麼就去認證使用者,並且重新產生新的一次性登錄資訊。 自動登錄的關鍵資訊一定是隻能使用一次,永遠不要重複使用一次性登錄資訊。
自動登錄資訊是長生命週期的認證資訊, 所以必須要儘可能的妥善保護。 可以對於自動登錄資訊對應的 cookie 設定路徑、僅允許 HTTP 訪問、僅允許安全訪問 等屬性來加以保護,並且僅在必需的時候才傳送這個 cookie。
開發者也要提供禁用自動登錄的機制, 以及刪除不再需要的自動登錄數據的能力。
CSRF(跨站請求偽造)
會話和認證無法避免跨站請求偽造攻擊。 開發者需要自己來實現保護應用不受 CSRF 攻擊的功能。
output_add_rewrite_var() 函式可以用來 保護應用免受 CSRF 攻擊。更多資訊請參考文件。
注意: PHP 7.2.0 之前的版本,這個函式和會話 ID 使用了同樣的輸出緩衝以及 INI 設定項, 所以不建議在 PHP 7.2.0 之前使用 output_add_rewrite_var() 函式。
大部分 Web 應用框架都提供了 CSRF 保護的特性。 詳細資訊請參考你所用的 Web 框架的文件。
從 PHP 7.3 開始,對於會話 cookie 增加了 SameSite 屬性, 這個屬性可以有效的降低 CSRF 攻擊的風險。