幾個月前,我在預(yù)發(fā)布環(huán)境上親眼看著一個機器人向 /admin/videos/delete 發(fā)起 POST,把測試播放列表清了個干凈。那個請求沒經(jīng)過任何表單認(rèn)證,卻帶著一個合法的會話憑據(jù)——瀏覽器對所有同源請求都會自動附上該憑據(jù),我的后臺也無條件信任了那個會話。
這是最純粹的跨站請求偽造(CSRF),而且對于 PHP 視頻管理面板來說,觸發(fā)起來難堪地容易:所有“審核通過”“刪除”“推上首頁”的操作,都是只靠登錄會話保護(hù)的狀態(tài)變更 POST。
我負(fù)責(zé) ViralVidVault 的后端。這是一家歐洲病毒視頻發(fā)現(xiàn)服務(wù),我們的管理后臺就是編輯團(tuán)隊審核爆款片段、調(diào)整分類權(quán)重、刷新緩存的地方。這些端點正是那種高價值、基于會話認(rèn)證,CSRF 首當(dāng)其沖的目標(biāo)。
這篇文章會講清楚,我們?nèi)绾卧?PHP 8.4 上用雙重提交令牌模式保護(hù)它們——為什么沒選服務(wù)器端的同步令牌、那個簽名細(xì)節(jié)幾乎每個教程都搞錯,以及跑在 LiteSpeed 和 Cloudflare 背后的生產(chǎn)代碼思路。
為什么不選經(jīng)典同步令牌?那種模式在服務(wù)器端會話中存一個隨機令牌,再比對隱藏表單域里的值。這能工作,但到我們這種規(guī)模,運維代價就上來了。每渲染一個表單都要讀寫會話狀態(tài)——這意味會話文件(或 Redis 鍵)在請求完成前被鎖定,對并發(fā) AJAX 批量操作的管理后臺來說,等于把同一用戶的請求都串行化了。我們的會話存在 WAL 模式的 SQLite 里,每次渲染表單都寫令牌,會帶來我們極力想避免的寫放大。而且在 Cloudflare 和 LiteSpeed 邊緣緩存體系下,任何強制每次請求都寫會話的東西,往往會迫出 Cache-Control: private,這種頭對管理后臺雖沒問題,但稍不留神就會泄漏到共享代碼路徑里。
雙重提交令牌模式完全繞開了服務(wù)器端存儲。思路很簡單:先發(fā)一個隨機令牌到客戶端(通過 Set-Cookie),再把這個令牌作為隱藏域(或 AJAX 的 X-CSRF-Token 頭)塞進(jìn)每個表單。收到狀態(tài)變更請求時,就拿客戶端發(fā)來的令牌和請求體里的令牌比較;一致就放行,否則拒絕。這個安全特性來自同源策略:攻擊者架在 evil.example 上的頁面可以強迫受害瀏覽器帶上我們的客戶端令牌,但 evil.example 上的 JavaScript 根本讀不到該令牌,自然沒法把它的值復(fù)制到請求體里。所以攻擊者頂多能滿足客戶端那一半,永遠(yuǎn)湊不齊請求體那一半。
問題在于,一個天真的雙提交實現(xiàn)是完全能被繞過的——只要攻擊者能在你域名上種一個客戶端令牌(比如借助被攻破的子域、未加密兄弟站點上的中間人,或者令牌注入漏洞)。修復(fù)辦法是對令牌做簽名,讓服務(wù)器能驗證這個令牌是自己簽發(fā)的。這就是 OWASP 所稱的“簽名雙重提交令牌”,也正是我們要走的路。而且這個簽名細(xì)節(jié),看過的教程里幾乎沒有一個做對的。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.