go語言csrf庫使用實現原理示例解析

引言

今天給大傢推薦的是web應用安全防護方面的一個包:csrf。該包為Go web應用中常見的跨站請求偽造(CSRF)攻擊提供預防功能。

csrf小檔案

「csrf小檔案」      
star 837 used by
contributors 25 作者 Gorilla
功能簡介 為Go web應用程序和服務提供跨站點請求偽造(csrf)預防功能。可作為gin、echo等主流框架的中間件使用。    
項目地址 github.com/gorilla/csr…    
相關知識 跨站請求偽造(CSRF)、contex.Contex、異或操作    

一、CSRF及其實現原理

CSRF是CROSS Site Request Forgy的縮寫,即跨站請求偽造。我們看下他的攻擊原理。如下圖:

當用戶訪問一個網站的時候,第一次登錄完成後,網站會將驗證的相關信息保存在瀏覽器的cookie中。在對該網站的後續訪問中,瀏覽器會自動攜帶該站點下的cookie信息,以便服務器校驗認證信息。

因此,當服務器經過用戶認證之後,服務器對後續的請求就隻認cookie中的認證信息,不再區分請求的來源瞭。那麼,攻擊者就可以模擬一個正常的請求來做一些影響正常用戶利益的事情(比如對於銀行來說可以把用戶的錢轉賬到攻擊者賬戶中。或獲取用戶的敏感、重要的信息等)

相關知識:因為登錄信息是基於session-cookie的。瀏覽器在訪問網站時會自動發送該網站的cookie信息,網站隻要能識別cookie中的信息,就會認為是認證已通過,而不會區分該請求的來源的。所以給攻擊者創造瞭攻擊的機會。

CSRF攻擊示例

假設有一個銀行網站A,下面的是一個轉給賬戶5000元的請求,使用Get方法

GET https://abank.com/transfer.do?account=RandPerson&amount=$5000 HTTP/1.1

然後,攻擊者修改瞭該請求中的參數,將收款賬戶更改成瞭自己的,如下:

GET https://abank.com/transfer.do?account=SomeAttacker&amount=$5000 HTTP/1.1

然後,攻擊者將該請求地址放入到一個標簽中:

<a href="https://abank.com/transfer.do?account=SomeAttacker&amount=$5000" rel="external nofollow" >Click for more information</a>

最後,攻擊者會以各種方式(放到自己的網站中、email、社交通訊工具等)引誘用戶點擊該鏈接。隻要是用戶點擊瞭該鏈接,並且在之前已經登錄瞭該網站,那麼瀏覽器就會將帶認證信息的cookie自動發送給該網站,網站認為這是一個正常的請求,由此,將給黑客轉賬5000元。造成合法用戶的損失。

當然,如果是post表單形式,那麼攻擊者會將偽造的鏈接放到form表達中,並用js的方法讓表單自動發送:

<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>

二、如何預防

常見的有3種方法:

  • 一種是在網站中增加對請求來源的驗證,比如在請求頭中增加REFFER信息。
  • 一種是在瀏覽器中啟用SameSite策略。該策略是告訴瀏覽器,隻有請求來源是同網站的才能發送cookie,跨站的請求不要發送cookie。但這種也有漏洞,就是依賴於瀏覽器是否支持這種策略。
  • 一種是使用Token信息。由網站自己決定token的生成策略以及對token的驗證。

其中使用Token信息這種是三種方法中最安全的一種。接下來我們就看看今天要推薦的CSRF包是如何利用token進行預防的。

三、CSRF包的使用及實現原理

csrf包的安裝

go get github.com/gorilla/csrf

基本使用

該包主要包括三個功能:

  • 通過csrf.Protect函數生成一個csrf中間件或請求處理器,用於後續的生成及校驗token的流程。
  • 通過csrf.Token函數,可以在響應中輸出當前生成的token值。
  • 通過csrf.TemplateField函數,可以在html模版中輸出一個hidden的input,用於在form表單中提交token。

該包的使用很簡單。首先通過csrf.Protect函數生成一個中間件或請求處理器,然後在啟動web server時對真實的請求處理器進行包裝。

我們來看下該包和主流web框架結合使用的實例。

使用net/http包啟動的服務

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函數將csrfMiddleware轉換成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轉換成gin的中間件返回值
 return adapter.Wrap(csrfMiddleware)
}
func main() {
 r := gin.New()
    // 在路由中使用中間件
 r.Use(CSRFMiddle())
    // 定義路由
 r.POST("/", IndexHandler)
    // 啟動http服務
 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轉換成gin的中間件返回值
 return csrfMiddleware
}

實際上,要通過token預防CSRF主要做以下3件事情:每次生成一個唯一的token、將token寫入到cookie同時下發給客戶端、校驗token。接下來我們就來看看csrf包是如何實現如上步驟的。

實現原理

csrf結構體

該包的實現是基於csrf這樣一個結構體:

type csrf struct {
 h    http.Handler
 sc   *securecookie.SecureCookie
 st   store
 opts options
}

該結構體同時實現瞭一個ServeHTTP方法:

func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request)

在Go中,我們知道ServeHTTP是在內建包net/http中定義的一個請求處理器的接口:

type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}

凡是實現瞭該接口的結構體就能作為請求的處理器。在go的所有web框架中,處理器本質上也都是基於該接口實現的。

好瞭,現在我們來分析下csrf這個結構體的成員:

  • 「h」:是一個http.Handler,作為實際處理請求的處理器。該h的來源是經Protect函數返回值包裝後的,即開始示例中CSRF(muxServer)中的muxServer。又因為csrf也是一個請求處理器,請求就會先執行csrf的ServeHTTP方法的邏輯,如果通過瞭,再執行h的ServeHTTP邏輯。
  • 「sc」:類型是*securecookie.SecureCookie,第三方包,該包的作用是對cookie的值進行加密/解密。在調用csrf.Protect方法時,傳遞的第一個32字節長的參數就是用於該包進行對稱加密用的秘鑰。下一篇文章我們會詳細介紹該包是如何實現對cookie內容進行/加解密的。
  • 「st」:類型是store,是csrf包中定義的一個接口類型。該屬性的作用是將token存儲在什麼地方。默認是使用cookieStore類型。即將token存儲在cookie中。
  • 「opts」:Options屬性,用於設置csrf的選項的。比如token存儲在cookie中的名字,token在表單中的名字等。

這裡大傢可能有這樣一個疑問:csrf攻擊就是基於cookie來進行攻擊的,為什麼還要把token存儲在cookie中呢?在一次請求中,會有兩個地方存儲token:一個是cookie中,一個是請求體中(query中,header中,或form中),當服務端收到請求時,會同時取出這兩個地方的token,進而進行比較。所以如果攻擊者偽造瞭一個請求,服務器能接收到cookie中的token,但不能接收到請求體中的token,所以偽造的攻擊還是無效的。

csrf包的工作流程

在開始的“使用net/http包啟動的服務”示例中,我們先調用瞭Protect方法,然後又用返回值對muxServer進行瞭包裝。大傢是不是有點雲裡霧裡,為什麼要這麼調用呢?接下來咱們就來分析下Protect這個函數以及csrf包的工作流程。

在使用csrf的時候,首先要調用的就是Protect函數。Protect的定義如下:

func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler

該函數接收一個秘鑰和一個選項切片參數。返回值是一個函數類型:func(http.Handler) http.Handler。實際的執行邏輯是在返回的函數中。如下:

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的實現源碼起始很簡單,就是在一個閉包中初始化瞭一個csrf結構體。示例中CSRF就是返回來的func(http.Handler) http.Handler函數。再調用CSRF(muxServer),執行初始化csrf結構體的實例,同時將muxServer包裝到csrf結構體的h屬性上,最後將該csrf結構體對象返回。因為csrf結構體也實現瞭ServeHTTP接口,所以csrf自然也就是可以處理請求的http.Handler類型瞭。

當一個請求來瞭之後,先執行csrf結構體中的ServeHTTP方法,然後再執行實際的http.Handler。以最開始的請求為例,csrf包的工作流程如下:

大致瞭解瞭csrf的工作流程後,我們再來分析各個環節的實現。

「生成唯一的token」

在該包中生成隨機、唯一的token是通過隨機數來生成的。主要生成邏輯如下:

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函數可以隨機生成指定字節個數的隨機數。但這裡出的隨機數是字節值,如果序列化成字符串則會是亂碼。那如何將字節序列序列化成可見的字符編碼呢?那就是對字節進行編碼。這裡使用的是標準庫中的encoding/json包。該包能夠對各種類型進行可視化編碼。如果對字節序列進行編碼,本質上是使用瞭base64的標準編碼。如下:

realToken := generateRandomBytes(32)
//編碼後,encodeToken是base64編碼的字符串
encodeToken := json.Encode(realToken)

「token的存儲位置」

生成token之後,token會存儲在兩個位置:

  • 一個是隨響應將token寫入cookie中。在cookie中的token將用於下次請求的基準token和請求中攜帶的token進行比較。該實現是通過csrf中的cookieStore來存儲到cookie中的(store類型)。在cookie中name默認是 _gorilla_csrf。同時,通過cookieStore類型存儲到cookie的值是經過加密的,加密使用的是securecookie.SecureCookie包
  • 一個是存儲在請求的上下文中。存在這裡的token是原始token經過轉碼的,會隨著響應下發給客戶端,以便下次請求時隨請求體一起發送。該實現是通過context.ValueContext存儲在請求的上下文中。

生成token後為什麼要存在cookie中呢?CSRF的攻擊原理不就是基於瀏覽器自動發送cookie造成的嗎?攻擊者偽造的請求還是會直接從cookie中獲取token,附帶在請求中不就行瞭嗎?答案是否定的。在請求中保存的token,是經過轉碼後的,跟cookie中的token不一樣。在收到請求時,再對token進行解碼,然後再和cookie中的token進行比較。看下下面的實現:

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一樣長度的隨機值otp,然後讓實際的realToken和opt通過xorToken進行異或操作,將異或操作的結果放到隨機值的末尾,然後再進行base64編碼產生的。

假設一個token是32位的字節,那麼最終的maskToken由64位組成。前32位是otp的隨機值,後32位是異或之後的token。兩個組合起來就是最終的maskToken。如下圖:

這裡利用瞭異或操作的原理來進行轉碼和解碼。我們假設A ^ B = C。那麼會有 A = C ^ B

所以,要想還原異或前的真實token值,則從maskToken中取出前32個字節和後32字節,再進行異或操作就能得到真實的token瞭。然後就可以和cookie中存儲的真實的token進行比較瞭。同時因為經過異或轉碼的token,攻擊者想要進行偽造就很難瞭。

「輸出token」

在上述我們已經知道經過異或操作對原始token進行瞭轉碼,我們叫做maskToken。該token要下發給客戶端(HEADER、form或其他位置)。那麼,客戶端用什麼字段來接收呢?

默認情況下,maskToken是存儲在以下位置的:

  • 若在HEADER頭中,則保存在名為  X-CSRF-Token 的字段中。
  • 若在form表單,則保存在名為 gorilla.csrf.Token 的input中。

當然,我們在初始化csrf的實例時,可以指定保存的位置。例如,我們指定HEADER頭中的字段名為 X-CSRF-Token-Request中,則可以使用如下代碼:

csrf.Protect([]byte("32-byte-long-auth-key"), 
             RequestHeader("X-CSRF-Token-Request"))

csrf中可以指定的選項如下:

  • RequestHeader選項函數:指定在HEADER中存儲token的字段名稱。
  • FieldName選項函數:指定form表中存儲token的input的name
  • MaxAge選項函數:指定cookie中值的有效期
  • Domain選項函數:指定cookie的存儲域名
  • Path選項函數:指定cookie的存儲路徑
  • HttpOnly選項函數:指定cookie的值隻能在服務端設置,禁止在客戶端使用javascript修改
  • SameSite選項函數:指定cookie的SameSite屬性
  • ErrorHandler選項函數:指定當token校驗不通過或生成token失敗時的錯誤響應的handler

「更新token」

在調用csrf.ServeHTTP函數中,每次都會生成一個新的token,存儲在對應的位置上,同時下發給客戶端,以便該請求的後續請求攜帶token值給服務端進行驗證。所以,該請求之前的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) {
 //這裡進行token的校驗
}

為什麼GET、HEAD、OPTIONS、TRACE方法的請求不需求token驗證呢?因為根據RFC7231文檔的規定,這些方法的請求本質上是一種 冪等 的訪問方法,這說明開發web的時候g這些請求不應該用於修改數據庫狀態,而隻作為一個請求訪問或者鏈接跳轉。通俗地講,發送一個GET請求不應該引起任何數據狀態的改變。用於修改狀態更加合適的是post方法,特別是對用戶信息狀態改變的情況。

所以,如果嚴格按照RFC的規定來開發的話,這些請求不應該修改數據,而隻是獲取數據。獲取數據對於攻擊者來說也沒實際價值。

總結

CSRF攻擊是基於將驗證信息存儲於cookie中,同時瀏覽器在發送請求時會自動攜帶cookie的原理進行的。所以,其預防原理也就是驗證請求來源的真實性。csrf包就是利用瞭token校驗的原理,讓前後連續的請求簽發token、下次請求驗證token的方式進行預防的。

以上就是go語言csrf庫使用實現原理示例解析的詳細內容,更多關於go語言csrf庫使用原理的資料請關註WalkonNet其它相關文章!

推薦閱讀: