HTTP 緩存別再亂用了!推薦一個緩存設置的最佳姿勢!
設置緩存大家可能大家都是從性能角度去考慮的,但是如果你不注意或者稍微設置不當,緩存也可能對我們的網(wǎng)站的安全性和用戶隱私造成負面影響。
開門見山
老規(guī)矩,先把推薦的配置說出來,后面再啰嗦:
- 為了防止中介緩存,建議設置:Cache-Control: private
- 建議設置適當?shù)亩壘彺?key:如果我們請求的響應是跟請求的 Cookie 相關的,建議設置:Vary: Cookie
那么為啥推薦這兩個配置呢?如果不配置會對我們的網(wǎng)站帶來什么風險呢?且聽我下面的講解。
回顧 HTTP 緩存
提到緩存,大家可能很快就會想到兩種緩存方式,以及對應的幾個請求頭,我們來快速回顧一下。
正常情況下,我們的瀏覽器客戶端會像服務器發(fā)起請求,然后服務器會將數(shù)據(jù)響應返回給客戶端。
但是一臺服務器可能要對成千上萬臺客戶端的請求進行響應,其中也有非常多是重復的請求,這會對服務器造成非常大的壓力。
所以一般我們都會在客戶端和服務器間進行一些緩存,對于一些重復的請求數(shù)據(jù),如果之前的響應已經(jīng)被存儲到緩存數(shù)據(jù)庫中,滿足一定條件的話就會直接去緩存中取,不會到達服務器。
那么,HTTP 緩存一般又分為兩種,強緩存和協(xié)商緩存:
強緩存
強緩存,在緩存數(shù)據(jù)未失效的情況下,客戶端可以直接使用緩存數(shù)據(jù),不用和數(shù)據(jù)庫進行交互。
那么,判斷請求是否失效主要靠兩個 HTTP Header:
- Expires:數(shù)據(jù)的緩存到期時間,下一次請求時,請求時間小于服務端返回的到期時間,直接使用緩存數(shù)據(jù)。
- Cache-Control:可以指定一個 max-age 字段,表示緩存的內(nèi)容將在一定時間后失效。
協(xié)商緩存
協(xié)商緩存,顧名思義需要和服務器進行一次協(xié)商。瀏覽器第一次請求時,服務器會將緩存標識與數(shù)據(jù)一起返回給客戶端,客戶端將二者備份至緩存數(shù)據(jù)庫中。
再次請求數(shù)據(jù)時,客戶端將備份的緩存標識發(fā)送給服務器,服務器根據(jù)緩存標識進行判斷,判斷成功后,返回 304 狀態(tài)碼,通知客戶端比較成功,可以使用緩存數(shù)據(jù)。
判斷請求主要靠下面兩組 HTTP Header:
- Last-Modified:一個 Response Header,服務器在響應請求時,告訴瀏覽器資源的最后修改時間。
- if-Modified-Since:一個 Request Header,再次請求服務器時,通過此字段通知服務器上次請求時,服務器返回的資源最后修改時間。
服務器會通過收到的 If-Modified-Since 和資源的最后修改時間進行比對,判斷是否使用緩存。
- Etag:一個 Response Header,服務器返回的資源的唯一標示
- If-None-Match:一個 Request Header,再次請求服務器時,通過此字段通知服務器客戶段緩存數(shù)據(jù)的唯一標識。
服務器會通過收到的 If-None-Match 和資源的唯一標識進行對比,判斷是否使用緩存。
關于緩存的常見誤區(qū)
上面提到的知識估計就是平時大家最常背到的,不過大家有沒有認真想過一個問題?我們?nèi)〉降木彺鏀?shù)據(jù),一定緩存在瀏覽器里面嗎?
實際上是不然的:資源的緩存通常是有多級的,一些緩存專門用于單個用戶,一些緩存專用于多個用戶。有些是由服務器控制的,有些是由用戶控制的,有些則由中介層控制。
- 瀏覽器緩存:一般并專用于單個用戶,在瀏覽器客戶端中實現(xiàn)。它們通過避免多次獲取相同的響應來提高性能。
- 本地代理:可能是用戶自己安裝的,也可能是由某個中介層管理的:比如公司的網(wǎng)絡層或者網(wǎng)絡提供商。本地代理通常會為多個用戶緩存單個響應,這就構成了一種“公共”緩存。
- 源服務器緩存/CDN。由服務器控制,源服務器緩存的目標是通過為多個用戶緩存相同的響應來減少源服務器的負載。CDN 的目標是相似的,但它分布在全球各個地區(qū),然后通過分配給最近的一組用戶來達到減少延遲的目的。
另外,我們也經(jīng)常會使用本地配置的代理,這些代理能夠通過配置信任證書來緩存 HTTPS 資源。
Spectre 漏洞
那么緩存怎么會對我們網(wǎng)站的安全性和用戶隱私造成威脅呢?我們來看一個非常有名的漏洞:Spectre。
攻擊者可以利用 Spectre 漏洞 來讀取操作系統(tǒng)進程的內(nèi)存,這意味著可以訪問到未經(jīng)過授權的跨域數(shù)據(jù)。
特別是在使用一些需要和計算機硬件進行交互的 API 時:
- SharedArrayBuffer (required for WebAssembly Threads)
- performance.measureMemory()
- JS Self-Profiling API
為此,瀏覽器一度禁用了 SharedArrayBuffer 等高風險的 API。
很多小伙伴對它具體的攻擊原理感興趣,通過幾個 JavaScript API 怎么做到越權訪問數(shù)據(jù)的?這個下次我會專門出個文章來講一下。
緩存是怎么影響 Spectre 的?
那么 Spectre 和緩存有啥關系呢?我們可以簡單的這樣理解下:
我們正常打開一個收到跨域限制的頁面,肯定是獲取不到數(shù)據(jù)的。但是如果我們的 Cache-Control 設置為了 Public,這份數(shù)據(jù)可能會被緩存到一個 Public Cache 上(比如我們本地代理的 Cache)。
雖然我們是沒有權限訪問這個數(shù)據(jù)的,但是數(shù)據(jù)卻被存到緩存數(shù)據(jù)庫里了。一旦數(shù)據(jù)已經(jīng)被存下來了,攻擊者就可以利用 Spectre 漏洞獲取到這些緩存數(shù)據(jù)了。
那么為啥利用 Spectre 可以越權訪問到這些緩存數(shù)據(jù)呢?我們來舉個簡單的小例子:
比如,我們有個網(wǎng)站的登錄密碼是 conardli,一個攻擊者想要爆破我們的密碼,假設我們的密碼一定由小寫字母組成,那攻擊者也至少需要 26 的 8 次方次來猜出我們的密碼。這是一個非常大的數(shù)字,幾乎不可能爆破成功。
假設,我們的密碼存在了一塊攻擊者無權限訪問到的內(nèi)存里,然后攻擊者自己又單獨使用一塊內(nèi)存存儲了所有的26個英文字母,并把這段內(nèi)存設置為不可緩存。
那么這個時候,攻擊者越界訪問了我們密碼的存儲區(qū)域,訪問到了 c 這個字母,但是由于權限問題,他肯定是訪問不到的,會被計算機拒絕。
但是雖然訪問不到,c 這個字母會被緩存起來。
這時,攻擊者再回去遍歷他那 26 個字母的內(nèi)存,會發(fā)現(xiàn),c 的訪問速度變快了 ...
所以,你的密碼第一個數(shù)字是 c ...
這里就簡單講一下,下篇文章我會專門來講一下 Spectre 漏洞,還是非常巧妙的 ... 感興趣的小伙伴可以再留言區(qū)告訴我。
網(wǎng)站的建議配置
因為上面的問題,我們建議對所有比較重要的網(wǎng)站數(shù)據(jù)做下面的兩個配置:
禁用 Public Cache
設置 Cache-Control: private,這可以禁用掉所有 Public Cache(比如代理),這就減少了攻擊者跨界訪問到公共內(nèi)存的可能性。
這里注意,private 這個值并不是一個獨立的值,比如他是可以和 max-age 進行共存的,性能和 public 差不了多少,我們打開 Google 的網(wǎng)站來看一下:
設置適當?shù)亩壘彺?key
默認情況下,我們?yōu)g覽器的緩存使用 URL 和 請求方法來做緩存 key 的。
這意味著,如果一個網(wǎng)站需要登錄,不同用戶的請求由于它們的請求URL和方法相同,數(shù)據(jù)會被緩存到一塊內(nèi)存里。
這顯然是有點問題,我們可以通過設置 Vary: Cookie 來避免這個問題。
當用戶身份信息發(fā)生變化的時候,緩存的內(nèi)存也會發(fā)生變化。
當然,如果你的資源是一個大家都可以訪問的公共 CDN 資源,那你的緩存當然是隨便設置了,如果你的資源數(shù)據(jù)是比較敏感的,建議走上面這兩個設置。