- 原文地址:Securing Cookies in Go
- 原文作者:Jon Calhoun
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:lsvih
- 校對者:tmpbook, Yuuoniy
在 Go 語言中增強 Cookie 的安全性
在我開始學習 Go 語言時已經有一些 Web 開發經驗了,但是並沒有直接操作 Cookie 的經驗。我之前做過 Rails 開發,當我不得不需要在 Rails 中讀寫 Cookie 時,並不需要自己去實現各種安全措施。
瞧瞧,Rails 預設就自己完成了大多數的事情。你不需要設定任何 CSRF 策略,也無需特別去加密你的 Cookie。在新版的 Rails 中,這些事情都是它預設幫你完成的。
而使用 Go 語言開發則完全不同。在 Golang 的預設設定中,這些事都不會幫你完成。因此,當你想要開始使用 Cookie 時,瞭解各種安全措施、為什麼要使用這些措施、以及如何將這些安全措施整合到你的應用中是非常重要的事。希望本文能幫助你做到這一點。
注意:我並不想引起關於 Go 與 Reils 兩者哪種更好的論戰。兩者各有優點,但在本文中我希望能著重討論 Cookie 的防護,而不是去爭論 Rails 和 Go 哪個好。
什麼是 Cookie?
在進入 Cookie 防護相關的內容前,我們必須要理解 Cookie 究竟是什麼。從本質上說,Cookie 就是儲存在終端使用者計算機中的鍵值對。因此,使用 Go 建立一個 Cookie 需要做的事就是建立一個包含鍵名、鍵值的 http.Cookie 型別欄位,然後呼叫 http.SetCookie 函式通知終端使用者的瀏覽器設定該 Cookie。
寫成程式碼之後,它看起來類似於這樣:
func someHandler(w http.ResponseWriter, r *http.Request) {
c := http.Cookie{
Name: "theme",
Value: "dark",
}
http.SetCookie(w, &c)
}複製程式碼
http.SetCookie
函式並不會返回錯誤,但它可能會靜默地移除無效的 Cookie,因此使用它並不是什麼美好的經歷。但它既然這麼設計了,就請你在使用這個函式的時候一定要牢記它的特性。
雖然這好像是在程式碼中“設定”了一個 Cookie,但其實我們只是在我們返回 Response 時傳送了一個 "Set-Cookie"
的 Header,從而定義需要設定的 Cookie。我們不會在伺服器上儲存 Cookie,而是依靠終端使用者的計算機建立與儲存 Cookie。
我要強調上面這一點,因為它存在非常嚴重的安全隱患:我們不能控制這些資料,而終端使用者的計算機(以及使用者)才能控制這些資料。
當讀取與寫入終端使用者控制的資料時,我們都需要十分謹慎地對資料進行處理。惡意使用者可以刪除 Cookie、修改儲存在 Cookie 中的資料,甚至我們可能會遇到中間人攻擊,即當使用者向伺服器傳送資料時,另有人試圖竊取 Cookie。
Cookie 的潛在安全問題
根據我的經驗,Cookie 相關的安全性問題大致分為以下五大類。下面我們先簡單地看一看,本文的剩餘部分將詳細討論每個分類的細節問題與解決對策。
1. Cookie 竊取 - 攻擊者會通過各種方式來試圖竊取 Cookie。我們將討論如何防範、規避這些方式,但是歸根結底我們並不能完全阻止裝置上的物理類接觸。
2. Cookie 篡改 - Cookie 中儲存的資料可以被使用者有意或無意地修改。我們將討論如何驗證儲存在 Cookie 中的資料確實是我們寫入的合法資料
3. 資料洩露 - Cookie 儲存在終端使用者的計算機上,因此我們需要清楚地意識到什麼資料是能儲存在 Cookie 中的,什麼資料是不能儲存在 Cookie 中的,以防其發生資料洩露。
4. 跨站指令碼攻擊(XSS) - 雖然這條與 Cookie 沒有直接關係,但是 XSS 攻擊在攻擊者能獲取 Cookie 時危害更大。我們應該考慮在非必須的時候限制指令碼訪問 Cookie。
5. 跨站請求偽造(CSRF) - 這種攻擊常常是由於使用 Cookie 儲存使用者登入會話造成的。因此我們將討論在這種情景下如何防範這種攻擊。
如我前面所說,在下文中我們將分別解決這些問題,讓你最終能夠專業地將你的 Cookie 裝進保險櫃。
Cookie 竊取
Cookie 竊取攻擊就和它字面意思一樣 —— 某人竊取了正常使用者的 Cookie,然後一般用來將自己偽裝成那個正常使用者。
Cookie 通常是被以下方式中的某種竊取:
- 中間人攻擊,或者是類似的其它攻擊方式,歸納一下就是攻擊者攔截你的 Web 請求,從中竊取 Cookie。
- 取得硬體的訪問許可權。
阻止中間人攻擊的終極方式就是當你的網站使用 Cookie 時,使用 SSL。使用 SSL 時,由於中間人無法對資料進行解密,因此外人基本上沒可能在請求的中途獲取 Cookie。
可能你會覺得“哈哈,中間人攻擊不太可能…”,我建議你看看 firesheep,這個簡單的工具,它足以說明在使用公共 wifi 時竊取未加密的 Cookie 是一件很輕鬆的事情。
如果你想確保這種事情不發生在你的使用者中,請使用 SSL!試試使用 Caddy Server 進行加密吧。它經過簡單的配置就能投入生產環境中。例如,你可以使用下面四行程式碼輕鬆讓你的 Go 應用使用代理:
calhoun.io {
gzip
proxy / localhost:3000
}複製程式碼
然後 Caddy 會為你自動處理所有與 SSL 有關的事務。
防範通過訪問硬體來竊取 Cookie 是十分棘手的事情。我們不能強制我們的使用者使用高安全性系統,也不能逼他們為電腦設定密碼,所以總會有他人坐在電腦前偷走 Cookie 的風險。此外,Cookie 也可能被病毒竊取,比如使用者開啟了某些釣魚郵件時就會出現這種情況。
不過這些都容易被發現。例如,如果有人偷了你的手錶,當你發現表不在手上時你立馬就會注意到它被偷了。然而 Cookie 還可以被複制,這樣任何人都不會意識到它已經丟了。
雖然不是萬無一失,但你還是可以用一些技術來猜測 Cookie 是否被盜了。例如,你可以追蹤使用者的登入裝置,要求他們重新輸入密碼。你還可以跟蹤使用者的 IP 地址,當其在可疑地點登入時通知使用者。
所有的這些解決方案都需要後端做更多的工作來追蹤資料,如果你的應用需要處理一些敏感資訊、金錢,或者它的收益可觀的話,請在安全方面投入更多精力。
也就是說,對於大多數只是作為過渡版本的應用來說,使用 SSL 就足夠了。
Cookie 篡改(也叫使用者偽造資料)
請直面這種情況 —— 可能有一些混蛋突然就想看看你設的 Cookie,然後修改它的值。也可能他是出於好奇才這麼做的,但是還是請你為這種可能發生的情況做好準備。
在一些情景中,我們對此並不在意。例如,我們給使用者定義一種主題設定時,並不會關心使用者是否改變了這個設定。當這個 Cookie 過期時,就會恢復預設的主題設定,並且如果使用者設定其為另一個有效的主題時我們可以讓他正常使用那個主題,這並不會對系統造成任何損失。
但是在另一些情況下,我們需要格外小心。編輯會話 Cookie 冒充另一個使用者產生的危害比改個主題大得多。我們絕不想看到張三假裝自己是李四。
我們將介紹兩種策略來檢測與防止 Cookie 被篡改。
1. 對資料進行數字簽名
對資料進行數字簽名,即對資料增加一個“簽名”,這樣能讓你校驗資料的可靠性。這種方法並不需要對終端使用者的資料進行加密或隱藏,只要對 Cookie 增加必要的簽名資料,我們就能檢測到使用者是否修改資料。
這種保護 Cookie 的方法原理是雜湊編碼 —— 我們對資料進行雜湊編碼,接著將資料與它的雜湊編碼同時存入 Cookie 中。當使用者傳送 Cookie 給我們時,再對資料進行雜湊計算,驗證此時的雜湊值與原始雜湊值是否匹配。
我們當然不會想看到使用者也建立一個新的雜湊來欺騙我們,因此你可以使用一些類似 HMAC 的雜湊演算法來使用祕鑰對資料進行雜湊編碼。這樣就能防範使用者同時編輯資料與數字簽名(即雜湊值)。
JSON Web Tokens(JWT) 預設內建了數字簽名功能,因此你可能對這種方法比較熟悉。
在 Go 中,可以使用類似 Gorilla 的 securecookie 之類的 package,你可以在建立 SecureCookie
時使用它來保護你的 Cookie。
// 推薦使用 32 位元組或 64 位元組的 hashKey
// 此處為了簡潔故設為了 “very-secret”
var hashKey = []byte("very-secret")
var s = securecookie.New(hashKey, nil)
func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
encoded, err := s.Encode("cookie-name", "cookie-value")
if err == nil {
cookie := &http.Cookie{
Name: "cookie-name",
Value: encoded,
Path: "/",
}
http.SetCookie(w, cookie)
fmt.Fprintln(w, encoded)
}
}複製程式碼
然後你可以在另一個處理 Cookie 的函式中同樣使用 SecureCookie 物件來讀取 Cookie。
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie("cookie-name"); err == nil {
var value string
if err = s.Decode("cookie-name", cookie.Value, &value); err == nil {
fmt.Fprintln(w, value)
}
}
}複製程式碼
以上樣例來源於 www.gorillatoolkit.org/pkg/securec….
注意:這兒的資料並不是進行了加密,而只是進行了編碼。我們會在“資料洩露”一章討論如何對資料進行加密。
這種模式還需要注意的是,如果你使用這種方式進行身份驗證,請遵循 JWT 的模式,將登入過期日期和使用者資料同時進行簽名。你不能只憑 Cookie 的過期日期來判斷登入是否有效,因為儲存在 Cookie 上的日期並未經過簽名,且使用者可以建立一個永不過期的新 Cookie,將原 Cookie 的內容複製進去就得到了一個永遠處於登入狀態的 Cookie。
2. 進行資料混淆
還有一種解決方案可以隱藏資料並防止使用者造假。例如,不要這樣儲存 Cookie:
// 別這麼做
http.Cookie{
Name: "user_id",
Value: "123",
}複製程式碼
我們可以儲存一個值來對映存在資料庫中的真實資料。通常使用 Session ID 或者 remember token 來作為這個值。例如我們有一個名為 remember_tokens
的表,這樣儲存資料:
remember_token: LAKJFD098afj0jasdf08jad08AJFs9aj2ASfd1
user_id: 123複製程式碼
在 Cookie 中,我們僅儲存這個 remember token。如果使用者想偽造 Cookie 也會無從下手。它看上去就是一堆亂碼。
之後當使用者要登陸我們的應用時,再根據 remember token 在資料庫中查詢,確定使用者具體的登入狀態。
為了讓此措施正常工作,你需要確保你的混淆值有以下特性:
- 能對映到使用者資料(或其它資源)
- 隨機
- 熵值高
- 可被無效化(例如在資料庫中刪除、修改 token 值)
這種方法也有一個缺點,就是在使用者訪問每個需要校驗許可權的頁面時都得進行資料庫查詢。不過這個缺點很少有人注意,而且可以通過快取等技術來減小資料庫查詢的開銷。這種方法的升級版就是 JWT,應用這種方法你可以隨時使會話無效化。
注意:儘管目前 JWT 收到了大多數 JS 框架的追捧,但上文這種方法是我瞭解的最常用的身份驗證策略。
資料洩露
在真正出現資料洩露前,通常需要另一種攻擊向量 —— 例如 Cookie 竊取。然而還是很難去正確地判斷並提防資料洩露的發生。因為僅僅是 Cookie 發生了洩露並不意味著攻擊者也得到了使用者的賬戶密碼。
無論何時,都應當減少儲存在 Cookie 中的敏感資料。絕不要將使用者密碼之類的東西存在 Cookie 中,即使密碼已經經過了編碼也不要這麼做。這篇文章 給出了幾個開發者無意間將敏感資料儲存在 Cookie 或 JWT 中的例項,由於(JWT 的 payload)是 base64 編碼,沒有經過任何加密,因此任何人都可以對其進行解碼。
出現資料洩露可是犯了大錯。如果你擔心你不小心儲存了一些敏感資料,我建議你使用如 Gorilla 的 securecookie 之類的 package。
前面我們討論瞭如何對你的 Cookie 進行數字簽名,其實 securecookie
也可以用於加密與解密你的 Cookie 資料,讓你的資料不能被輕易地解碼並讀取。
使用這個 package 進行加密,你只需要在建立 SecureCookie
例項時傳入一個“塊祕鑰”(blockKey)即可。
var hashKey = []byte("very-secret")
// 增加這一部分進行加密
var blockKey = []byte("a-lot-secret")
var s = securecookie.New(hashKey, blockKey)複製程式碼
其它所有東西都和前面章節的數字簽名中的樣例一致。
再次提醒,你不應該在 Cookie 中儲存任何敏感資料,尤其不能儲存密碼之類的東西。加密僅僅是一項為資料增加一部分安全性,使其成為”半敏感資料“資料的技術而已。
跨站指令碼攻擊(XSS)
跨站指令碼(Cross-site scripting)也經常被記為 XSS,及有人試圖將一些不是你寫的 JavaScript 程式碼注入你的網站中。但由於其攻擊的機理,你無法知道正在瀏覽器中執行的 JavaScript 程式碼到底是不是你的伺服器提供的程式碼。
無論何時,你都應該儘量去阻止 XSS 攻擊。在本文中我們不會深入探討這種攻擊的具體細節,但是以防萬一我建議你在非必要的情況下禁止 JavaScript 訪問 Cookie 的許可權。在你需要這個許可權的時候你可以隨時開啟它,所以不要讓它成為你的網站安全性脆弱的理由。
在 Go 中完成這點很簡單,只需要在建立 Cookie 時設定 HttpOnly
欄位為 true 即可。
cookie := http.Cookie{
// true 表示指令碼無許可權,只允許 http request 使用 Cookie。
// 這與 Http 與 Https 無關。
HttpOnly: true,
}複製程式碼
CSRF(跨站請求偽造)
CSRF 發生的情況為某個使用者訪問別人的站點,但那個站點有一個能提交到你的 web 應用的表單。由於終端使用者提交表單時的操作不經由指令碼,因此瀏覽器會將此請求設為使用者進行的操作,將 Cookie 附上表單資料同時傳送。
乍一看似乎這沒什麼問題,但是如果外部網站傳送一些使用者不希望傳送的資料時會發生什麼呢?例如,badsite.com 中有個表單,會提交請求將你的 100 美元轉到他們的賬戶中,而 chase.com 希望你在它這兒登入你的銀行賬戶。這可能會導致在終端使用者不知情的情況下錢被轉走。
Cookie 不會直接導致這樣的問題,不過如果你使用 Cookie 作為身份驗證的依據,那你需要使用 Gorilla 的 csrf 之類的 package 來避免 CSRF 攻擊。
這個 package 將會提供一個 CSRF token,插入你網站的每個表單中,當表單中不含 token 時,csrf
package 中介軟體將會阻止表單的提交,使得別的網站不能欺騙使用者在他們那兒向你的網站提交表單。
更多關於 CSRF 攻擊的資料請參閱:
在非必要時限制 Cookie 的訪問許可權
我們要討論的最後一件事與特定的攻擊無關,更像是一種指導原則。我建議在使用 Cookie 時儘量限制其許可權,僅在你需要時開發相關許可權。
前面討論 XSS 時我也簡單的提到過這點,但一般的觀點是你需要儘可能限制對 Cookie 的訪問。例如,如果你的 Web 應用沒有使用子域名,那你就不應該賦予 Cookie 所有子域的許可權。不過這是 Cookie 的預設值,因此其實你什麼都不用做就能將 Cookie 的許可權限制在某個特定域中。
但是,如果你需要與子域共享 Cookie,你可以這麼做:
c := Cookie{
// 根據主機模式的預設設定,Cookie 進行的是精確域名匹配。
// 因此請僅在需要的時候開啟子域名許可權!
// 下面的程式碼可以讓 Cookie 在 yoursite.com 的任何子域下工作:
Domain: "yoursite.com",
}複製程式碼
欲瞭解更多有關域的資訊,請參閱 tools.ietf.org/html/rfc626…。你也可以在這兒閱讀原始碼,參閱其預設設定:golang.org/src/net/htt….
你可以參閱 這個 stackoverflow 的問題 瞭解更多資訊,弄明白為什麼在為子域使用 Cookie 時不需要提供子域字首.此外 Go 原始碼連結中也可以看到如果你提供字首名的話會被自動去除。
除了將 Cookie 的許可權限制在特定域上之外,你還可以將 Cookie 限制於某個特定的目錄路徑中。
c := Cookie{
// Defaults 設定為可訪問應用的任何路徑,但你也可以
// 進行如下設定將其限制在特定子目錄下:
Path: "/app/",
}複製程式碼
還有你也可以對其設定路徑字首,例如 /blah/
,你可以參閱下面這篇文章瞭解更多這個欄位的使用方法:tools.ietf.org/html/rfc626….
為什麼我不使用 JWT?
就知道肯定會有人提出這個問題,下面讓我簡單解釋一下。
可能有很多人和你說過,Cookie 的安全性與 JWT 一樣。但實際上,Cookie 與 JWT 解決的並不是相同的問題。比如 JWT 可以儲存在 Cookie 中,這和將其放在 Header 中的實際效果是一樣的。
另外,Cookie 可用於無需驗證的資料,在這種情況下了解如何增加 Cookie 的安全性也是必要的。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。