會話管理基礎(chǔ)

會話安全

會話模塊無法保證你存儲在會話中的信息只能被創(chuàng)建會話的用戶本人可見。 你需要采取額外的手段來保護(hù)會話中的機(jī)密信息, 至于采取何種方式來保護(hù)機(jī)密信息, 取決于你在會話中存儲的數(shù)據(jù)的機(jī)密程度。

評估會話中存儲的數(shù)據(jù)的重要性, 以及為此增加額外的保護(hù)機(jī)制, 通常需要付出一定的代價(jià),同時(shí)會降低便利性。 例如,如果你需要保護(hù)用戶免受社會工程學(xué)攻擊, 你需要啟用 session.use_only_cookies 選項(xiàng)。 這就要求用戶在使用過程中,必須把瀏覽器設(shè)置為接受 cookie, 否則就無法正常使用會話功能了。

有很多種方式都可以導(dǎo)致會話 ID 被泄露給第三方。 例如,JavaScript 注入,URL 中包含會話 ID,數(shù)據(jù)包偵聽, 或者直接訪問你的物理設(shè)備等。 如果會話 ID 被泄漏給第三方, 那么他們就可以訪問這個(gè)會話 ID 可以訪問的全部資源。 首先,如果在 URL 中包含了會話 ID, 并且訪問了外部的站點(diǎn), 那么你的會話 ID 可能在外部站點(diǎn)的訪問日志中被記錄(referrer 請求頭)。 另外,攻擊者也可以監(jiān)聽你的網(wǎng)絡(luò)通信,如果通信未加密, 那么會話 ID 將會在網(wǎng)絡(luò)中以明文的形式進(jìn)行傳輸。 針對這種情況的解決方案就是在服務(wù)端配置 SSL/TLS, 另外,使用 HSTS 可以達(dá)到更高的安全性。

注意: 即使使用 HTTPS 協(xié)議,也無法百分百保證機(jī)密數(shù)據(jù)不被泄漏。 例如,CRIME 和 BEAST 漏洞可以使得攻擊者讀取到你的數(shù)據(jù)。 另外,出于網(wǎng)絡(luò)通信審計(jì)目的,很多網(wǎng)絡(luò)中都存在 HTTPS MITM 代理, 可以讀取 HTTPS 協(xié)議下的通信數(shù)據(jù)。 那么攻擊者也可以搭建類似的代理服務(wù)器,用來竊取 HTTPS 協(xié)議下的通信數(shù)據(jù)。

嚴(yán)格會話管理

目前,默認(rèn)情況下,PHP 是以自適應(yīng)的方式來管理會話的, 這種方式使用起來很靈活,但是同樣也帶來了一定的風(fēng)險(xiǎn)。

新增加了一個(gè)配置項(xiàng): session.use_strict_mode。 當(dāng)啟用這個(gè)配置項(xiàng),并且你所用的會話存儲處理器支持的話,未經(jīng)初始化的會話 ID 會被拒絕, 并為其生成一個(gè)全新的會話,這可以避免攻擊者使用一個(gè)已知的會話 ID 來進(jìn)行攻擊。 例如,攻擊者可以通過郵件給受害者發(fā)送一個(gè)包含會話 ID 的鏈接: http://example.com/page.php?PHPSESSID=123456789。 如果啟用了 session.use_trans_sid 配置項(xiàng), 那么受害者將會使用攻擊者所提供的會話 ID 開始一個(gè)新的會話。 如果啟用了 session.use_strict_mode 選項(xiàng),就可以降低風(fēng)險(xiǎn)。

警告

用戶自定義的會話存儲器也可以通過實(shí)現(xiàn)會話 ID 驗(yàn)證來支持嚴(yán)格會話模式。 建議用戶在實(shí)現(xiàn)自己的會話存儲器的時(shí)候, 一定要對會話 ID 的合法性進(jìn)行驗(yàn)證。

在瀏覽器一側(cè),可以為用來保存會話 ID 的 cookie 設(shè)置域,路徑, 僅允許 HTTP 訪問,必須使用 HTTPS 訪問等安全屬性。 如果使用的是 PHP 7.3. 版本,還可以對 cookie 設(shè)置 SameSite 屬性。 攻擊者可以利用瀏覽器的這些特性來設(shè)置永久可用的會話 ID。 僅僅設(shè)置 session.use_only_cookies 配置項(xiàng) 無法解決這個(gè)問題。而 session.use_strict_mode 配置項(xiàng) 可以降低這種風(fēng)險(xiǎn)。設(shè)置 session.use_strict_mode=On, 來拒絕未經(jīng)初始化的會話 ID。

注意: 雖然使用 session.use_strict_mode 配置項(xiàng) 可以降低靈活會話管理方式所帶來的風(fēng)險(xiǎn), 攻擊者還是通過利用 JavaScript 注入等手段, 強(qiáng)制用戶使用由攻擊者創(chuàng)建的并且經(jīng)過了正常的初始化的會話 ID。 如何降低這種風(fēng)向,可以參考本手冊的建議部分。 如果你已經(jīng)啟用了 session.use_strict_mode 配置項(xiàng), 同時(shí)使用基于時(shí)間戳的會話管理, 并且通過設(shè)置 session_regenerate_id() 配置項(xiàng) 來重新生成會話 ID, 那么,攻擊者生成的會話 ID 就可以被刪除掉了。 當(dāng)發(fā)生對過期會話訪問的時(shí)候, 你應(yīng)該保存活躍會話的所有數(shù)據(jù), 以備后續(xù)分析使用。 然后讓用戶退出當(dāng)前的會話,并且重新登錄。 防止攻擊者繼續(xù)使用“偷”來的會話。

警告

對過期會話數(shù)據(jù)的訪問并不總是意味著正在遭受攻擊。 不穩(wěn)定的網(wǎng)絡(luò)狀況,或者不正確的會話刪除行為, 都會導(dǎo)致合法的用戶產(chǎn)生訪問過期會話數(shù)據(jù)的情況。

從 PHP 7.1.0 開始,增加了 session_create_id() 函數(shù)。 這個(gè)函數(shù)允許開發(fā)者在會話 ID 中增加用戶 ID 作為前綴, 以確保用戶訪問到正確對應(yīng)的會話數(shù)據(jù)。 要使用這個(gè)函數(shù), 請確保啟用了 session.use_strict_mode 配置項(xiàng), 否則惡意用戶可能會偽造其他用戶的會話 ID。

注意: 對于 PHP 7.1.0 之前的用戶,應(yīng)該使用 CSPRNG(例如 /dev/urandom) 或者 random_bytes() 函數(shù)以及哈希函數(shù) 來產(chǎn)生新的會話 ID。 session_create_id() 函數(shù)本身包含碰撞檢測的能力, 并且根據(jù) INI 文件中和會話相關(guān)的配置項(xiàng)來生成會話 ID。 所以,建議使用 session_create_id() 函數(shù)來生成會話 ID。

重新生成會話 ID

雖然 session.use_strict_mode 配置項(xiàng)可以降低風(fēng)險(xiǎn),但是還不夠。為了確保會話安全,開發(fā)者還需要使用 session_regenerate_id() 函數(shù)。

會話 ID 重生機(jī)制可以有效的降低會話被竊取的風(fēng)險(xiǎn), 所以,必須周期性的調(diào)用 session_regenerate_id() 函數(shù) 來重新生成會話 ID, 例如,對于機(jī)密內(nèi)容,每隔 15 分鐘就重新生成會話 ID。 這樣一來,即使會話 ID 被竊取, 那么攻擊者所得到的會話 ID 也會很快的過期, 如果他們進(jìn)一步訪問,就會產(chǎn)生對過期會話數(shù)據(jù)訪問的錯(cuò)誤。

當(dāng)用戶成功通過認(rèn)證之后,必須為其重新生成會話 ID。 并且,必須在向 $_SESSION 中保存用戶認(rèn)證信息之前 調(diào)用 session_regenerate_id() 函數(shù)( session_regenerate_id() 函數(shù) 會自動將重生之前的會話數(shù)據(jù)保存到新生成的會話)。 請確保只有新的會話包含用戶認(rèn)證信息。

開發(fā)者不可過分依賴 session.gc_maxlifetime 配置項(xiàng)。 因?yàn)楣粽呖梢栽谑芎φ叩臅掃^期之前訪問系統(tǒng), 并且維持這個(gè)會話的活動,以保證這個(gè)會話不會過期。

實(shí)際上,你需要自己實(shí)現(xiàn)基于時(shí)間戳的會話數(shù)據(jù)管理機(jī)制。

警告

雖然會話管理器可以透明的管理時(shí)間戳,但是這個(gè)特性尚未完整的實(shí)現(xiàn)。 在 GC 發(fā)生之前,舊的會話數(shù)據(jù)還得保存, 同時(shí),開發(fā)者還得保證過期的會話數(shù)據(jù)已經(jīng)被移除。 但是,開發(fā)者又不能立即移除活躍會話中的數(shù)據(jù)。 所以,不要同時(shí)在活躍會話上調(diào)用 session_regenerate_id(true);session_destroy() 函數(shù)。 這聽起來有點(diǎn)兒自相矛盾,但是事實(shí)上必須得這么做。

默認(rèn)情況下,session_regenerate_id() 函數(shù) 不會刪除舊的會話, 所以即使重生了會話 ID,舊的會話可能還是可用的。 開發(fā)者需要使用時(shí)間戳等機(jī)制, 來確保舊的會話數(shù)據(jù)不會再次被訪問。

警告

刪除活躍會話可能會帶來非預(yù)期的一些影響。 例如,在網(wǎng)絡(luò)狀態(tài)不穩(wěn)定,或者有并發(fā)請求到達(dá) Web 服務(wù)器的情況下, 立即刪除活躍會話可能導(dǎo)致個(gè)別請求會話失效的問題。

立即刪除活躍會話也無法檢測可能存在的惡意訪問。

作為替代方案, 你要在 $_SESSION 中設(shè)置一個(gè)很短的過期時(shí)間, 然后根據(jù)這個(gè)時(shí)間戳來判斷后續(xù)的訪問是被允許的還是被禁止的。

在調(diào)用 session_regenerate_id() 函數(shù)之后, 不能立即禁止對舊的會話數(shù)據(jù)的訪問,應(yīng)該再一小段之間之后再禁止訪問。 例如,在穩(wěn)定的網(wǎng)絡(luò)條件下,可以設(shè)置為幾秒鐘, 在不穩(wěn)定的網(wǎng)絡(luò)條件下,可以設(shè)置為幾分鐘。

如果用戶訪問了舊的會話數(shù)據(jù)(已經(jīng)過期的), 那么應(yīng)該禁止訪問。 建議從會話中移除這個(gè)用戶的認(rèn)證信息,因?yàn)檫@看起來像是在遭受攻擊。

如果攻擊者設(shè)置了不可刪除的 cookie,那么使用 session.use_only_cookiessession_regenerate_id() 會導(dǎo)致正常用戶遭受拒絕服務(wù)的問題。 如果發(fā)生這種情況,請讓用戶刪除 cookie 并且警告用戶他可能面臨一些安全問題。 攻擊者可以通過惡意的 Web 應(yīng)用、瀏覽器插件以及對安全性較差的物理設(shè)備進(jìn)行攻擊 來偽造惡意的 cookie。

警告

請勿誤解這里的拒絕服務(wù)攻擊風(fēng)險(xiǎn)所指的含義。 通常來講,要保護(hù)會話 ID 的安全,use_strict_mode=On 是必須要做的。 建議所有的站點(diǎn)都啟用 use_strict_mode=On。

只有當(dāng)賬號處于被攻擊的時(shí)候,才會發(fā)生拒絕服務(wù)的問題。 通常都是由于應(yīng)用中被注入了惡意的 JavaScript 才會導(dǎo)致這個(gè)問題。

會話中數(shù)據(jù)的刪除

過期的會話中的數(shù)據(jù)應(yīng)該是被刪除的,并且不可訪問。 現(xiàn)在的會話模塊尚未很好的支持這種特性。

應(yīng)該盡可能快的刪除過期會話中的數(shù)據(jù)。 但是,活躍會話一定不要立即刪除。 為了能夠同時(shí)滿足這兩點(diǎn)要求, 你需要自己來實(shí)現(xiàn)基于時(shí)間戳的會話數(shù)據(jù)管理機(jī)制。

在 $_SESSION 中設(shè)置會話過期時(shí)間戳,并且對其進(jìn)行管理, 以便能夠阻止對于過期會話的訪問。 當(dāng)發(fā)生對于過期會話的訪問時(shí),建議從相關(guān)用戶的所有會話中刪除認(rèn)證信息, 并且要求用戶重新認(rèn)證。 對于過期會話數(shù)據(jù)的訪問可能是一種攻擊行為, 為了保護(hù)會話數(shù)據(jù),你需要追蹤每個(gè)用戶的活躍會話。

注意: 當(dāng)用戶處于不穩(wěn)定的網(wǎng)絡(luò),或者 web 應(yīng)用存在并發(fā)的請求的時(shí)候, 也可能發(fā)生對于過期會話數(shù)據(jù)的訪問。 服務(wù)器嘗試為用戶設(shè)置新的會話 ID, 但是很可能由于網(wǎng)絡(luò)原因,導(dǎo)致 Set-Cookie 的數(shù)據(jù)包無法到達(dá)用戶的瀏覽器。 當(dāng)通過 session_regenerate_id() 函數(shù) 為一個(gè)連接生成新的會話 ID 之后,其他的并發(fā)連接可能尚未得到這個(gè)新的會話 ID。 因此,不能立即阻止對于過期會話數(shù)據(jù)的訪問,而是要延遲一個(gè)很小的時(shí)間段, 這就是為什么我們需要實(shí)現(xiàn)基于時(shí)間戳的會話管理。

簡而言之,不要在調(diào)用 session_regenerate_id() 或者 session_destroy() 函數(shù)的時(shí)候立即刪除舊的會話數(shù)據(jù), 而是要通過一個(gè)時(shí)間戳來控制后續(xù)對于這個(gè)舊會話數(shù)據(jù)的訪問。 從會話存儲中刪除數(shù)據(jù)的工作交給 session_gc() 函數(shù)來完成吧。

會話和鎖定

默認(rèn)情況下,為了保證會話數(shù)據(jù)在多個(gè)請求之間的一致性, 對于會話數(shù)據(jù)的訪問是加鎖進(jìn)行的。

但是,這種鎖定機(jī)制也會導(dǎo)致被攻擊者利用,來進(jìn)行對于用戶的拒絕服務(wù)攻擊。 為了降低這種風(fēng)險(xiǎn),請?jiān)谠L問會話數(shù)據(jù)的時(shí)候,盡可能的縮短鎖定的時(shí)間。 當(dāng)某個(gè)請求不需要更新會話數(shù)據(jù)的時(shí)候,使用只讀模式訪問會話數(shù)據(jù)。 也就是說,在調(diào)用 session_start() 函數(shù)的時(shí)候, 使用 'read_and_close' 選項(xiàng):session_start(['read_and_close'=>1]);。 另外,如果需要更新會話數(shù)據(jù),那么在更新完畢之后, 馬上調(diào)用 session_commit() 函數(shù)來釋放對于會話數(shù)據(jù)的鎖。

當(dāng)會話不活躍的時(shí)候,當(dāng)前的會話模塊不會檢測對于 $_SESSION 的修改。 你需要自己來保證 在會話處于不活躍狀態(tài)的時(shí)候,不要去修改它。

活躍會話

開發(fā)者需要自己來追蹤每個(gè)用戶的活躍會話, 要知道每個(gè)用戶創(chuàng)建了多少活躍會話,每個(gè)活躍會話來自那個(gè) IP 地址, 活躍了多長時(shí)間等。 PHP 不會自動完成這項(xiàng)工作,需要開發(fā)者來完成。

有很多種方式可以做到追蹤用戶的活躍會話。 你可以通過在數(shù)據(jù)庫中存儲會話信息來跟蹤用戶會話。 由于會話是可以被垃圾收集器收集掉的, 所以你也需要處理被收集掉的會話數(shù)據(jù), 以保證數(shù)據(jù)庫中的數(shù)據(jù)和真實(shí)的活躍會話數(shù)據(jù)的一致性。

一種很簡單的方式就是使用“使用用戶 ID 作為會話 ID 前綴”,并且保存必要的信息到 $_SESSION 中。 大部分的數(shù)據(jù)庫產(chǎn)品對于字符串前綴查詢(譯注:也即右模糊查詢,可以利用索引)都有很好的性能表現(xiàn)。 為了實(shí)現(xiàn)這種方式,可以使用 session_regenerate_id()session_create_id() 函數(shù)。

警告

永遠(yuǎn)不要使用機(jī)密數(shù)據(jù)作為會話 ID 前綴! 如果用戶 ID 屬于機(jī)密數(shù)據(jù),那么可以考慮使用 hash_hmac() 函數(shù)對其進(jìn)行摘要后再使用。

警告

必須啟用 session.use_strict_mode 配置項(xiàng)。 請確保已經(jīng)啟用, 否則活躍會話數(shù)據(jù)庫可能會被入侵。

要能夠檢測對于過期會話數(shù)據(jù)的訪問, 基于時(shí)間戳的會話數(shù)據(jù)管理機(jī)制是必不可少的。 當(dāng)檢測到對于過期會話數(shù)據(jù)的訪問時(shí),你應(yīng)該從相關(guān)用戶的活躍會話中刪除認(rèn)證信息, 避免攻擊者持續(xù)使用盜取的會話。

會話和自動登錄

開發(fā)者不應(yīng)該通過使用長生命周期的會話 ID 來實(shí)現(xiàn)自動登錄功能, 因?yàn)檫@種方式提高了會話被竊取的風(fēng)險(xiǎn)。 開發(fā)者應(yīng)該自己實(shí)現(xiàn)自動登錄的機(jī)制。

在使用 setcookie() 的時(shí)候,傳入安全的一次性摘要結(jié)果作為自動登錄信息。 建議使用比 SHA-2 更高強(qiáng)度的摘要算法(例如 SHA-256) 對 random_bytes() 隨機(jī)生成的數(shù)據(jù) (也可以讀取 /dev/urandom 設(shè)備)進(jìn)行摘要作為自動登錄的信息。

在用戶訪問的時(shí)候,如果發(fā)現(xiàn)用戶尚未認(rèn)證, 那么就去檢查請求中是否包含了有效的一次性登錄信息。 如果包含有效的一次性登錄信息,那么就去認(rèn)證用戶,并且重新生成新的一次性登錄信息。 自動登錄的關(guān)鍵信息一定是只能使用一次,永遠(yuǎn)不要重復(fù)使用一次性登錄信息。

自動登錄信息是長生命周期的認(rèn)證信息, 所以必須要盡可能的妥善保護(hù)。 可以對于自動登錄信息對應(yīng)的 cookie 設(shè)置路徑、僅允許 HTTP 訪問、僅允許安全訪問 等屬性來加以保護(hù),并且僅在必需的時(shí)候才傳送這個(gè) cookie。

開發(fā)者也要提供禁用自動登錄的機(jī)制, 以及刪除不再需要的自動登錄數(shù)據(jù)的能力。

CSRF(跨站請求偽造)

會話和認(rèn)證無法避免跨站請求偽造攻擊。 開發(fā)者需要自己來實(shí)現(xiàn)保護(hù)應(yīng)用不受 CSRF 攻擊的功能。

output_add_rewrite_var() 函數(shù)可以用來 保護(hù)應(yīng)用免受 CSRF 攻擊。更多信息請參考文檔。

注意: PHP 7.2.0 之前的版本,這個(gè)函數(shù)和會話 ID 使用了同樣的輸出緩沖以及 INI 設(shè)置項(xiàng), 所以不建議在 PHP 7.2.0 之前使用 output_add_rewrite_var() 函數(shù)。

大部分 Web 應(yīng)用框架都提供了 CSRF 保護(hù)的特性。 詳細(xì)信息請參考你所用的 Web 框架的文檔。

從 PHP 7.3 開始,對于會話 cookie 增加了 SameSite 屬性, 這個(gè)屬性可以有效的降低 CSRF 攻擊的風(fēng)險(xiǎn)。