Go 語言安全編程系列(一):CSRF 攻擊防護

1、工作原理
在 Go Web 編程中,我們可以基于第三方 gorilla/csrf 包避免 CSRF 攻擊,和 Laravel 框架一樣,這也是一個基于 HTTP 中間件避免 CSRF 攻擊的解決方案,其中包含的中間件名稱是 csrf.Protect。
注:CSRF 全名是 Cross-Site Request Forgery,即跨站請求偽造,這是一種通過偽裝授權(quán)用戶的請求來攻擊授信網(wǎng)站的惡意漏洞。
我們來看看 csrf.Protect 是如何工作的:
當(dāng)我們在路由器上應(yīng)用這個中間件后,當(dāng)請求到來時,會通過 csrf.Token 函數(shù)生成一個令牌(Token)以便發(fā)送給 HTTP 響應(yīng)(可以是 HTML 表單也可以是 JSON 響應(yīng)),對于 HTML 表單視圖,可以向視圖模板傳遞一個注入令牌值的輔助函數(shù) csrf.TemplateField,然后我們就可以在客戶端通過?{{?.csrfField }} 將包含令牌值的隱藏字段發(fā)送給服務(wù)端,服務(wù)端通過驗證客戶端發(fā)送的令牌值和服務(wù)端保存的令牌值是否一致來驗證請求來自授信客戶端,從而達到避免 CSRF 攻擊的目的。
gorilla/csrf 被設(shè)計為兼容當(dāng)前流行的開源組件和框架,比如 Gorilla 工具集、net/http 包、Goji、Gin、Echo 等。
2、使用示例
接下來,學(xué)院君來簡單演示下如何在實際項目中使用 gorilla/csrf 提供的 csrf.Protect 中間件。
HTML 表單
首先是 HTML 表單,csrf.Protect 中間件使用起來非常簡單,你只需要在啟動 Web 服務(wù)器時將其應(yīng)用到路由器上即可,然后在渲染表單視圖時傳遞帶有令牌信息的 csrf.TemplateField 函數(shù):
package?main
import?(
????"github.com/gorilla/csrf"
????"github.com/gorilla/mux"
????"html/template"
????"net/http"
)
func?main()?{
????//?初始化路由器
????r?:=?mux.NewRouter()
????//?注冊表單頁面路由(GET)
????r.HandleFunc("/signup",?ShowSignupForm)
????//?提交注冊表單路由(POST)
????//?如果請求字段不包含有效的?CSRF?令牌,則返回?403?響應(yīng)
????r.HandleFunc("/signup/post",?SubmitSignupForm).Methods("POST")
????//?應(yīng)用?csrf.Protect?中間件到路由器?r
????//?該函數(shù)第一個參數(shù)是?32?位長的認(rèn)證密鑰(任意字符做?MD5?元算即可),用于加密?CSRF?令牌
????//?本地開發(fā)基于?HTTP?協(xié)議,所以第二個參數(shù)通過?csrf.Secure(false)?進行標(biāo)識
????http.ListenAndServe(":8000",
????????csrf.Protect([]byte("251e79cd5d1a994c51fd316f7040f13d"),?csrf.Secure(false))(r))
}
//?注冊表單頁面處理器
func?ShowSignupForm(w?http.ResponseWriter,?r?*http.Request)?{
????//?傳遞注入?CSRF?令牌的?csrf.TemplateField?函數(shù)到注冊頁面
????t?:=?template.Must(template.ParseFiles("signup.html"))
????t.Execute(w,?map[string]interface{}{
????????csrf.TemplateTag:?csrf.TemplateField(r),
????})
????//?我們還可以通過 csrf.Token(r)?直接獲取令牌并將其設(shè)置到請求頭:w.Header.Set("X-CSRF-Token", token)
????//?這在發(fā)送?JSON?響應(yīng)到客戶端或者前端?JavaScript?框架時很有用
}
//?提交注冊表單處理器
func?SubmitSignupForm(w?http.ResponseWriter,?r?*http.Request)?{
????//?暫不做任何處理
}
然后我們在在同級目錄下新建 signup.html,通過 {{ .csrfField }} 渲染隱藏的令牌字段:
<html?lang="en">
<head>
????<meta?charset="UTF-8">
????<title>Signuptitle>
head>
<body>
????<form?action="/signup/post"?method="post">
????????{{?.csrfField?}}
????????<input?type="text"?name="name">
????????<input?type="password"?name="password">
????????<hr/>
????????<button?id="submit">Submitbutton>
????form>
body>
html>
啟動 Web 服務(wù)器,在瀏覽器中訪問 http://127.0.0.1:8000/signup,就可以通過源代碼查看到隱藏的包含 CSRF 令牌的輸入框了:

如果我們試圖刪除這個輸入框或者變更 CSRF 令牌的值,提交表單,就會返回 403 響應(yīng)了:

錯誤信息是 CSRF 令牌值無效。
JavaScript 應(yīng)用
csrf.Protect 中間件還適用于前后端分離的應(yīng)用,此時后端數(shù)據(jù)以接口方式提供給前端,不再有視圖模板的渲染,設(shè)置中間件的方式不變,但是傳遞 CSRF 令牌給客戶端的方式要調(diào)整:
package?main
import?(
????"encoding/json"
????"github.com/gorilla/csrf"
????"github.com/gorilla/mux"
????"net/http"
)
type?User?struct?{
????Id?string?`json:"id"`
????Name?string?`json:"name"`
????Website?string?`json:"website"`
}
func?main()?{
????r?:=?mux.NewRouter()
????//?設(shè)置路由前綴
????api?:=?r.PathPrefix("/api").Subrouter()
????//?在子路由上應(yīng)用?csrf.Protect?中間件
????api.Use(csrf.Protect([]byte("251e79cd5d1a994c51fd316f7040f13d")))
????//?如果客戶端?JavaScript?應(yīng)用部署在其他域名,需要通過?csrf.TrustedOrigins?設(shè)置服務(wù)端信任的客戶端域名列表
????//?api.Use(csrf.Protect([]byte("251e79cd5d1a994c51fd316f7040f13d"),?csrf.TrustedOrigins([]string{"cli.domain.com"})))
????//?獲取用戶信息接口路由
????api.HandleFunc("/user/{id}",?GetUser).Methods("GET")
????http.ListenAndServe(":8000",?r)
}
func?GetUser(w?http.ResponseWriter,?r?*http.Request)?{
????//?從路由參數(shù)中讀取用戶?id,再從數(shù)據(jù)庫查詢對應(yīng)用戶信息
????//?這里我們簡單模擬下用戶數(shù)據(jù)進行測試即可
????id?:=?r.FormValue("id")
????user?:=?User{Id:?id,?Name:?"學(xué)院君",?Website:?"https://xueyuannjun.com"}
????//?獲取令牌值并將其設(shè)置到響應(yīng)頭
????//?這樣一來,咱們的?JSON?客戶端或者?JavaScript?框架就可以讀取響應(yīng)頭獲取?CSRF?令牌值
????//?然后在后續(xù)發(fā)送?POST?請求時就可以通過?X-CSRF-Token?請求頭中帶上這個?CSRF?令牌
????w.Header().Set("X-CSRF-Token",?csrf.Token(r))
????b,?err?:=?json.Marshal(user)
????if?err?!=?nil?{
????????http.Error(w,?err.Error(),?500)
????????return
????}
????w.Write(b)
}
我們啟動 Web 服務(wù)器,請求 /api/user/1 接口,就可以獲取如下響應(yīng)信息:

這樣一來,我們就可以在客戶端讀取響應(yīng)頭中的 CSRF 令牌信息了,以 Axios 庫為例,客戶端可以這樣發(fā)送包含 CSRF 令牌的 POST 請求:
//?你可以從響應(yīng)頭中讀取?CSRF?令牌,也可以將其存儲到單頁面應(yīng)用的某個全局標(biāo)簽里
//?然后從這個標(biāo)簽中讀取 CSRF 令牌值,比如這里就是這么做的:
let?csrfToken?=?document.getElementsByName("gorilla.csrf.Token")[0].value
//?初始化?Axios?請求頭,包含域名、超時和?CSRF?令牌信息
const?instance?=?axios.create({
??baseURL:?"https://domain.com/api/",
??timeout:?1000,
??headers:?{?"X-CSRF-Token":?csrfToken?}
})
//?這樣一來,后續(xù)發(fā)送的所有?HTTP?請求都會包含?CSRF?令牌
try?{
??let?resp?=?await?instance.post(endpoint,?formData)
??//?處理響應(yīng)
}?catch?(err)?{
??//?處理異常
}
關(guān)于 Go Web 編程中的 CSRF 攻擊防護我們就簡單介紹到這里,更多細節(jié),請參考 gorilla/csrf 項目官方文檔:https://github.com/gorilla/csrf。
(全文完)
推薦閱讀

