go語言csrf庫使用實現(xiàn)原理示例解析
引言
今天給大家推薦的是web應(yīng)用安全防護(hù)方面的一個包:csrf。該包為Go web應(yīng)用中常見的跨站請求偽造(CSRF)攻擊提供預(yù)防功能。
csrf小檔案
「csrf小檔案」 | |||
---|---|---|---|
star | 837 | used by | - |
contributors | 25 | 作者 | Gorilla |
功能簡介 | 為Go web應(yīng)用程序和服務(wù)提供跨站點請求偽造(csrf)預(yù)防功能??勺鳛間in、echo等主流框架的中間件使用。 | ||
項目地址 | github.com/gorilla/csr… | ||
相關(guān)知識 | 跨站請求偽造(CSRF)、contex.Contex、異或操作 |
一、CSRF及其實現(xiàn)原理
CSRF是CROSS Site Request Forgy的縮寫,即跨站請求偽造。我們看下他的攻擊原理。如下圖:
當(dāng)用戶訪問一個網(wǎng)站的時候,第一次登錄完成后,網(wǎng)站會將驗證的相關(guān)信息保存在瀏覽器的cookie中。在對該網(wǎng)站的后續(xù)訪問中,瀏覽器會自動攜帶該站點下的cookie信息,以便服務(wù)器校驗認(rèn)證信息。
因此,當(dāng)服務(wù)器經(jīng)過用戶認(rèn)證之后,服務(wù)器對后續(xù)的請求就只認(rèn)cookie中的認(rèn)證信息,不再區(qū)分請求的來源了。那么,攻擊者就可以模擬一個正常的請求來做一些影響正常用戶利益的事情(比如對于銀行來說可以把用戶的錢轉(zhuǎn)賬到攻擊者賬戶中?;颢@取用戶的敏感、重要的信息等)
相關(guān)知識:因為登錄信息是基于session-cookie的。瀏覽器在訪問網(wǎng)站時會自動發(fā)送該網(wǎng)站的cookie信息,網(wǎng)站只要能識別cookie中的信息,就會認(rèn)為是認(rèn)證已通過,而不會區(qū)分該請求的來源的。所以給攻擊者創(chuàng)造了攻擊的機(jī)會。
CSRF攻擊示例
假設(shè)有一個銀行網(wǎng)站A,下面的是一個轉(zhuǎn)給賬戶5000元的請求,使用Get方法
GET https://abank.com/transfer.do?account=RandPerson&amount=$5000 HTTP/1.1
然后,攻擊者修改了該請求中的參數(shù),將收款賬戶更改成了自己的,如下:
GET https://abank.com/transfer.do?account=SomeAttacker&amount=$5000 HTTP/1.1
然后,攻擊者將該請求地址放入到一個標(biāo)簽中:
<a rel="external nofollow" >Click for more information</a>
最后,攻擊者會以各種方式(放到自己的網(wǎng)站中、email、社交通訊工具等)引誘用戶點擊該鏈接。只要是用戶點擊了該鏈接,并且在之前已經(jīng)登錄了該網(wǎng)站,那么瀏覽器就會將帶認(rèn)證信息的cookie自動發(fā)送給該網(wǎng)站,網(wǎng)站認(rèn)為這是一個正常的請求,由此,將給黑客轉(zhuǎn)賬5000元。造成合法用戶的損失。
當(dāng)然,如果是post表單形式,那么攻擊者會將偽造的鏈接放到form表達(dá)中,并用js的方法讓表單自動發(fā)送:
<body onload="document.forms[0].submit()> <form id=”csrf” action="https://abank.com/transfer.do" method="POST"> <input type="hidden" name="account" value="SomeAttacker"/> <input type="hidden" name="amount" value="$5000"/> </form> </body> <script> document.getElementById('csrf').submit(); </script>
二、如何預(yù)防
常見的有3種方法:
- 一種是在網(wǎng)站中增加對請求來源的驗證,比如在請求頭中增加REFFER信息。
- 一種是在瀏覽器中啟用SameSite策略。該策略是告訴瀏覽器,只有請求來源是同網(wǎng)站的才能發(fā)送cookie,跨站的請求不要發(fā)送cookie。但這種也有漏洞,就是依賴于瀏覽器是否支持這種策略。
- 一種是使用Token信息。由網(wǎng)站自己決定token的生成策略以及對token的驗證。
其中使用Token信息這種是三種方法中最安全的一種。接下來我們就看看今天要推薦的CSRF包是如何利用token進(jìn)行預(yù)防的。
三、CSRF包的使用及實現(xiàn)原理
csrf包的安裝
go get github.com/gorilla/csrf
基本使用
該包主要包括三個功能:
- 通過csrf.Protect函數(shù)生成一個csrf中間件或請求處理器,用于后續(xù)的生成及校驗token的流程。
- 通過csrf.Token函數(shù),可以在響應(yīng)中輸出當(dāng)前生成的token值。
- 通過csrf.TemplateField函數(shù),可以在html模版中輸出一個hidden的input,用于在form表單中提交token。
該包的使用很簡單。首先通過csrf.Protect函數(shù)生成一個中間件或請求處理器,然后在啟動web server時對真實的請求處理器進(jìn)行包裝。
我們來看下該包和主流web框架結(jié)合使用的實例。
使用net/http包啟動的服務(wù)
package main import ( "fmt" "github.com/gorilla/csrf" "net/http" ) func main() { muxServer := http.NewServeMux() muxServer.HandleFunc("/", IndexHandler) CSRF := csrf.Protect([]byte("32-byte-long-auth-key")) http.ListenAndServe(":8000", CSRF(muxServer)) } func IndexHandler(w http.ResponseWriter, r *http.Request) { // 獲取token值 token := csrf.Token(r) // 將token寫入到header中 w.Header().Set("X-CSRF-Token", token) fmt.Fprintln(w, "hello world.Go") }
echo框架下使用csrf包
package main import ( "github.com/gorilla/csrf" "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() e.POST("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) // 使用自定義的CSRF中間件 e.Use(CSRFMiddle()) e.Logger.Fatal(e.Start(":8080")) } // 自定義CSRF中間件 func CSRFMiddle() echo.MiddlewareFunc { csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key")) // 這里使用echo的WrapMiddleware函數(shù)將csrfMiddleware轉(zhuǎn)換成echo的中間件返回值 return echo.WrapMiddleware(csrfMiddleware) }
gin框架下使用csrf包
import ( "fmt" "github.com/gin-gonic/gin" "github.com/gorilla/csrf" adapter "github.com/gwatts/gin-adapter" ) // 定義中間件 func CSRFMiddle() gin.HandlerFunc { csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key")) // 這里使用adpater包將csrfMiddleware轉(zhuǎn)換成gin的中間件返回值 return adapter.Wrap(csrfMiddleware) } func main() { r := gin.New() // 在路由中使用中間件 r.Use(CSRFMiddle()) // 定義路由 r.POST("/", IndexHandler) // 啟動http服務(wù) r.Run(":8080") } func IndexHandler(ctx *gin.Context) { ctx.String(200, "hello world") }
beego框架下使用csrf包
package main import ( "github.com/beego/beego" "github.com/gorilla/csrf" ) func main() { beego.Router("/", &MainController{}) beego.RunWithMiddleWares(":8080", CSRFMiddle()) } type MainController struct { beego.Controller } func (this *MainController) Get() { this.Ctx.Output.Body([]byte("Hello World")) } func CSRFMiddle() beego.MiddleWare { csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key")) // 這里使用adpater包將csrfMiddleware轉(zhuǎn)換成gin的中間件返回值 return csrfMiddleware }
實際上,要通過token預(yù)防CSRF主要做以下3件事情:每次生成一個唯一的token、將token寫入到cookie同時下發(fā)給客戶端、校驗token。接下來我們就來看看csrf包是如何實現(xiàn)如上步驟的。
實現(xiàn)原理
csrf結(jié)構(gòu)體
該包的實現(xiàn)是基于csrf這樣一個結(jié)構(gòu)體:
type csrf struct { h http.Handler sc *securecookie.SecureCookie st store opts options }
該結(jié)構(gòu)體同時實現(xiàn)了一個ServeHTTP方法:
func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request)
在Go中,我們知道ServeHTTP是在內(nèi)建包net/http中定義的一個請求處理器的接口:
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
凡是實現(xiàn)了該接口的結(jié)構(gòu)體就能作為請求的處理器。在go的所有web框架中,處理器本質(zhì)上也都是基于該接口實現(xiàn)的。
好了,現(xiàn)在我們來分析下csrf這個結(jié)構(gòu)體的成員:
- 「h」:是一個http.Handler,作為實際處理請求的處理器。該h的來源是經(jīng)Protect函數(shù)返回值包裝后的,即開始示例中CSRF(muxServer)中的muxServer。又因為csrf也是一個請求處理器,請求就會先執(zhí)行csrf的ServeHTTP方法的邏輯,如果通過了,再執(zhí)行h的ServeHTTP邏輯。
- 「sc」:類型是*securecookie.SecureCookie,第三方包,該包的作用是對cookie的值進(jìn)行加密/解密。在調(diào)用csrf.Protect方法時,傳遞的第一個32字節(jié)長的參數(shù)就是用于該包進(jìn)行對稱加密用的秘鑰。下一篇文章我們會詳細(xì)介紹該包是如何實現(xiàn)對cookie內(nèi)容進(jìn)行/加解密的。
- 「st」:類型是store,是csrf包中定義的一個接口類型。該屬性的作用是將token存儲在什么地方。默認(rèn)是使用cookieStore類型。即將token存儲在cookie中。
- 「opts」:Options屬性,用于設(shè)置csrf的選項的。比如token存儲在cookie中的名字,token在表單中的名字等。
這里大家可能有這樣一個疑問:csrf攻擊就是基于cookie來進(jìn)行攻擊的,為什么還要把token存儲在cookie中呢?在一次請求中,會有兩個地方存儲token:一個是cookie中,一個是請求體中(query中,header中,或form中),當(dāng)服務(wù)端收到請求時,會同時取出這兩個地方的token,進(jìn)而進(jìn)行比較。所以如果攻擊者偽造了一個請求,服務(wù)器能接收到cookie中的token,但不能接收到請求體中的token,所以偽造的攻擊還是無效的。
csrf包的工作流程
在開始的“使用net/http包啟動的服務(wù)”示例中,我們先調(diào)用了Protect方法,然后又用返回值對muxServer進(jìn)行了包裝。大家是不是有點云里霧里,為什么要這么調(diào)用呢?接下來咱們就來分析下Protect這個函數(shù)以及csrf包的工作流程。
在使用csrf的時候,首先要調(diào)用的就是Protect函數(shù)。Protect的定義如下:
func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler
該函數(shù)接收一個秘鑰和一個選項切片參數(shù)。返回值是一個函數(shù)類型:func(http.Handler) http.Handler。實際的執(zhí)行邏輯是在返回的函數(shù)中。如下:
CSRF := csrf.Protect([]byte("32-byte-long-auth-key")) http.ListenAndServe(":8000", CSRF(muxServer)) // Protect源碼 func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { cs := parseOptions(h, opts...) // Set the defaults if no options have been specified if cs.opts.ErrorHandler == nil { cs.opts.ErrorHandler = http.HandlerFunc(unauthorizedHandler) } if cs.opts.MaxAge < 0 { // Default of 12 hours cs.opts.MaxAge = defaultAge } if cs.opts.FieldName == "" { cs.opts.FieldName = fieldName } if cs.opts.CookieName == "" { cs.opts.CookieName = cookieName } if cs.opts.RequestHeader == "" { cs.opts.RequestHeader = headerName } // Create an authenticated securecookie instance. if cs.sc == nil { cs.sc = securecookie.New(authKey, nil) // Use JSON serialization (faster than one-off gob encoding) cs.sc.SetSerializer(securecookie.JSONEncoder{}) // Set the MaxAge of the underlying securecookie. cs.sc.MaxAge(cs.opts.MaxAge) } if cs.st == nil { // Default to the cookieStore cs.st = &cookieStore{ name: cs.opts.CookieName, maxAge: cs.opts.MaxAge, secure: cs.opts.Secure, httpOnly: cs.opts.HttpOnly, sameSite: cs.opts.SameSite, path: cs.opts.Path, domain: cs.opts.Domain, sc: cs.sc, } } return cs } }
Protect的實現(xiàn)源碼起始很簡單,就是在一個閉包中初始化了一個csrf結(jié)構(gòu)體。示例中CSRF就是返回來的func(http.Handler) http.Handler
函數(shù)。再調(diào)用CSRF(muxServer),執(zhí)行初始化csrf結(jié)構(gòu)體的實例,同時將muxServer包裝到csrf結(jié)構(gòu)體的h屬性上,最后將該csrf結(jié)構(gòu)體對象返回。因為csrf結(jié)構(gòu)體也實現(xiàn)了ServeHTTP接口,所以csrf自然也就是可以處理請求的http.Handler類型了。
當(dāng)一個請求來了之后,先執(zhí)行csrf結(jié)構(gòu)體中的ServeHTTP方法,然后再執(zhí)行實際的http.Handler。以最開始的請求為例,csrf包的工作流程如下:
大致了解了csrf的工作流程后,我們再來分析各個環(huán)節(jié)的實現(xiàn)。
「生成唯一的token」
在該包中生成隨機(jī)、唯一的token是通過隨機(jī)數(shù)來生成的。主要生成邏輯如下:
func generateRandomBytes(n int) ([]byte, error) { b := make([]byte, n) _, err := rand.Read(b) // err == nil only if len(b) == n if err != nil { return nil, err } return b, nil }
crypto/rand包中的rand.Read函數(shù)可以隨機(jī)生成指定字節(jié)個數(shù)的隨機(jī)數(shù)。但這里出的隨機(jī)數(shù)是字節(jié)值,如果序列化成字符串則會是亂碼。那如何將字節(jié)序列序列化成可見的字符編碼呢?那就是對字節(jié)進(jìn)行編碼。這里使用的是標(biāo)準(zhǔn)庫中的encoding/json包。該包能夠?qū)Ω鞣N類型進(jìn)行可視化編碼。如果對字節(jié)序列進(jìn)行編碼,本質(zhì)上是使用了base64的標(biāo)準(zhǔn)編碼。如下:
realToken := generateRandomBytes(32) //編碼后,encodeToken是base64編碼的字符串 encodeToken := json.Encode(realToken)
「token的存儲位置」
生成token之后,token會存儲在兩個位置:
- 一個是隨響應(yīng)將token寫入cookie中。在cookie中的token將用于下次請求的基準(zhǔn)token和請求中攜帶的token進(jìn)行比較。該實現(xiàn)是通過csrf中的cookieStore來存儲到cookie中的(store類型)。在cookie中name默認(rèn)是
_gorilla_csrf
。同時,通過cookieStore類型存儲到cookie的值是經(jīng)過加密的,加密使用的是securecookie.SecureCookie包 - 一個是存儲在請求的上下文中。存在這里的token是原始token經(jīng)過轉(zhuǎn)碼的,會隨著響應(yīng)下發(fā)給客戶端,以便下次請求時隨請求體一起發(fā)送。該實現(xiàn)是通過context.ValueContext存儲在請求的上下文中。
生成token后為什么要存在cookie中呢?CSRF的攻擊原理不就是基于瀏覽器自動發(fā)送cookie造成的嗎?攻擊者偽造的請求還是會直接從cookie中獲取token,附帶在請求中不就行了嗎?答案是否定的。在請求中保存的token,是經(jīng)過轉(zhuǎn)碼后的,跟cookie中的token不一樣。在收到請求時,再對token進(jìn)行解碼,然后再和cookie中的token進(jìn)行比較。看下下面的實現(xiàn):
func mask(realToken []byte, r *http.Request) string { otp, err := generateRandomBytes(tokenLength) if err != nil { return "" } // XOR the OTP with the real token to generate a masked token. Append the // OTP to the front of the masked token to allow unmasking in the subsequent // request. return base64.StdEncoding.EncodeToString(append(otp, xorToken(otp, realToken)...)) }
這里我們看到,先生成一個和token一樣長度的隨機(jī)值otp,然后讓實際的realToken和opt通過xorToken進(jìn)行異或操作,將異或操作的結(jié)果放到隨機(jī)值的末尾,然后再進(jìn)行base64編碼產(chǎn)生的。
假設(shè)一個token是32位的字節(jié),那么最終的maskToken由64位組成。前32位是otp的隨機(jī)值,后32位是異或之后的token。兩個組合起來就是最終的maskToken。如下圖:
這里利用了異或操作的原理來進(jìn)行轉(zhuǎn)碼和解碼。我們假設(shè)A ^ B = C
。那么會有 A = C ^ B
所以,要想還原異或前的真實token值,則從maskToken中取出前32個字節(jié)和后32字節(jié),再進(jìn)行異或操作就能得到真實的token了。然后就可以和cookie中存儲的真實的token進(jìn)行比較了。同時因為經(jīng)過異或轉(zhuǎn)碼的token,攻擊者想要進(jìn)行偽造就很難了。
「輸出token」
在上述我們已經(jīng)知道經(jīng)過異或操作對原始token進(jìn)行了轉(zhuǎn)碼,我們叫做maskToken。該token要下發(fā)給客戶端(HEADER、form或其他位置)。那么,客戶端用什么字段來接收呢?
默認(rèn)情況下,maskToken是存儲在以下位置的:
- 若在HEADER頭中,則保存在名為 X-CSRF-Token 的字段中。
- 若在form表單,則保存在名為 gorilla.csrf.Token 的input中。
當(dāng)然,我們在初始化csrf的實例時,可以指定保存的位置。例如,我們指定HEADER頭中的字段名為 X-CSRF-Token-Request中,則可以使用如下代碼:
csrf.Protect([]byte("32-byte-long-auth-key"), RequestHeader("X-CSRF-Token-Request"))
csrf中可以指定的選項如下:
- RequestHeader選項函數(shù):指定在HEADER中存儲token的字段名稱。
- FieldName選項函數(shù):指定form表中存儲token的input的name
- MaxAge選項函數(shù):指定cookie中值的有效期
- Domain選項函數(shù):指定cookie的存儲域名
- Path選項函數(shù):指定cookie的存儲路徑
- HttpOnly選項函數(shù):指定cookie的值只能在服務(wù)端設(shè)置,禁止在客戶端使用javascript修改
- SameSite選項函數(shù):指定cookie的SameSite屬性
- ErrorHandler選項函數(shù):指定當(dāng)token校驗不通過或生成token失敗時的錯誤響應(yīng)的handler
「更新token」
在調(diào)用csrf.ServeHTTP函數(shù)中,每次都會生成一個新的token,存儲在對應(yīng)的位置上,同時下發(fā)給客戶端,以便該請求的后續(xù)請求攜帶token值給服務(wù)端進(jìn)行驗證。所以,該請求之前的token也就失效了。
為什么GET、HEAD、OPTIONS、TRACE的請求方法不需要token驗證
在csrf包中,我們還看到有這么一段判斷邏輯:
// Idempotent (safe) methods as defined by RFC7231 section 4.2.2. safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"} if !contains(safeMethods, r.Method) { //這里進(jìn)行token的校驗 }
為什么GET、HEAD、OPTIONS、TRACE方法的請求不需求token驗證呢?因為根據(jù)RFC7231文檔的規(guī)定,這些方法的請求本質(zhì)上是一種 冪等 的訪問方法,這說明開發(fā)web的時候g這些請求不應(yīng)該用于修改數(shù)據(jù)庫狀態(tài),而只作為一個請求訪問或者鏈接跳轉(zhuǎn)。通俗地講,發(fā)送一個GET請求不應(yīng)該引起任何數(shù)據(jù)狀態(tài)的改變。用于修改狀態(tài)更加合適的是post方法,特別是對用戶信息狀態(tài)改變的情況。
所以,如果嚴(yán)格按照RFC的規(guī)定來開發(fā)的話,這些請求不應(yīng)該修改數(shù)據(jù),而只是獲取數(shù)據(jù)。獲取數(shù)據(jù)對于攻擊者來說也沒實際價值。
總結(jié)
CSRF攻擊是基于將驗證信息存儲于cookie中,同時瀏覽器在發(fā)送請求時會自動攜帶cookie的原理進(jìn)行的。所以,其預(yù)防原理也就是驗證請求來源的真實性。csrf包就是利用了token校驗的原理,讓前后連續(xù)的請求簽發(fā)token、下次請求驗證token的方式進(jìn)行預(yù)防的。
以上就是go語言csrf庫使用實現(xiàn)原理示例解析的詳細(xì)內(nèi)容,更多關(guān)于go語言csrf庫使用原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang實現(xiàn)加權(quán)輪詢負(fù)載均衡算法
加權(quán)輪詢負(fù)載均衡算法是一種常見的負(fù)載均衡策略,本文主要介紹了Golang實現(xiàn)加權(quán)輪詢負(fù)載均衡算法,具有一定的參考價值,感興趣的可以了解一下2024-08-08解決Golang map range遍歷結(jié)果不穩(wěn)定問題
這篇文章主要介紹了解決Golang map range遍歷結(jié)果不穩(wěn)定問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12Go 代碼規(guī)范錯誤處理示例經(jīng)驗總結(jié)
這篇文章主要為大家介紹了Go 代碼規(guī)范錯誤處理示例實戰(zhàn)經(jīng)驗總結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08