基於gin的golang web開發:永遠不要相信使用者的輸入

陳巨集博發表於2020-11-26

作為後端開發者我們要記住一句話:“永遠不要相信使用者的輸入”,這裡所說的使用者可能是人,也可能是另一個應用程式。“永遠不要相信使用者的輸入”是安全編碼的準則,也就是說,任何輸入的內容在驗證無害之前都是有害的。很多應用程式的安全漏洞都和使用者輸入有關,比如SQL隱碼攻擊漏洞。

我們可以通過引數驗證、sql語句過濾和引數化查詢等方式對使用者的輸入進行處理來規避這種安全隱患。本文介紹第一種方法,並對基於gin的golang web開發:模型驗證進行補充,瞭解更多的引數驗證方法。

驗證非必填的郵箱欄位

需求是這樣的:我們需要驗證一個欄位可以為空,同時欄位的值為合法的電子郵箱。

type MailRequest struct {
	Email    string `json:"email" binding:"email"` // 郵箱地址
}

程式碼的執行結果和我們想的不太一樣,我們沒有為欄位設定required標籤,但是傳入空字串時會提示Email必須是一個有效的郵箱,解決方法是加入omitempty驗證規則。omitempty允許條件驗證,在沒有為欄位設定值的情況下,跳過後面的驗證規則。注意omitempty要放在其他規則前面。下面是修改後的程式碼:

type MailRequest struct {
	Email    string `json:"email" binding:"omitempty,email"` // 郵箱地址
}

驗證0值

先看程式碼

type AddRoleRequest struct {
	Available int `json:"available" binding:"required"` // 是否可用 0 不可用 1 可用
}

Available 欄位為int型別,新增了required驗證規則,0為一個有效的值。Available為0時不能通過Gin的引數驗證。這裡只需要把欄位型別修改為*int即可。

type AddRoleRequest struct {
	Available *int `json:"available" binding:"required"` // 是否可用 0 不可用 1 可用
}

自定義錯誤訊息

前文基於gin的golang web開發:模型驗證結尾部分,我們沒有把引數驗證的錯誤訊息完全翻譯成中文,欄位名還是英文的。顯然還有更優雅的做法,給使用者提示一個更友好的錯誤資訊。

{
    "error": "Username為必填欄位;"
}

返回值中的Username為欄位名稱,可以通過自定義標籤的方式修改錯誤資訊中的欄位名。我們自定義一個display標籤,然後使用標籤的值替換掉驗證器中的欄位。

func init() {
	translator := zh.New()
	uni = ut.New(translator, translator)
	trans, _ = uni.GetTranslator("zh")
	validate := binding.Validator.Engine().(*validator.Validate)
	// 注意這裡
	validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
		return fld.Tag.Get("display")
	})
	_ = zh_translations.RegisterDefaultTranslations(validate, trans)
}

func Translate(err error) string {
	var result string

	errors := err.(validator.ValidationErrors)

	for _, err := range errors {
		errMessage := err.Translate(trans)
		result += errMessage + ";"
	}
	return result[:len(result)-1] // <--
}

type AddUserRequest struct {
    Username string `json:"username" binding:"required" display:"使用者名稱"`
	Password string `json:"password" binding:"required" display:"密碼"` // 登入密碼
	Nickname string `json:"nickname" binding:"required" display:"暱稱"` // 暱稱
}

注意程式碼中validate.RegisterTagNameFunc方法註冊display標籤,Translate方法也有一些改進,去掉了結果中最後一個分號。

舉個例子

func checkUser(user string, password string) bool {
	db := GetDbContext()
	defer db.Close()

	dataSql := `
select count(1) from sys_user
where username = '` + user + `' and password = '` + password + `'`
	count := 0
	log.Println(dataSql)
	db.QueryRow(dataSql).Scan(&count)
	return count > 0
}

這段程式碼用於判斷賬號密碼是否正確,但是沒有驗證使用者輸入的user和password引數,惡意使用者構造一個特殊的密碼1' or '1'='1dataSql中拼接的sql語句變為

select count(1) from sys_user
where username = 'xxx' and password = '1' or '1'='1'

查詢結果大於0,方法返回真。這就造成了sql注入。我們可以在password欄位上增加規則alphanum驗證欄位內容只能為字母或數字。1' or '1'='1不能通過引數驗證也就規避掉了SQL隱碼攻擊的問題。例子中拼接SQL語句是為了方便演示,正式專案中不推薦這種寫法。完整程式碼如下:

type UserAndPassword struct {
	User string `json:"user" binding:"required,alphanum"`
	Pwd  string `json:"pwd" binding:"required,alphanum"`
}

func IsLoginIn(c *gin.Context) {
	var req = UserAndPassword{}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.String(http.StatusOK, strconv.FormatBool(checkUser(req.User, req.Pwd)))
}

func checkUser(user string, password string) bool {
	db := GetDbContext()
	defer db.Close()

	dataSql := `
select count(1) from sys_user
where username = '` + user + `' and password = '` + password + `'`
	count := 0
	log.Println(dataSql)
	db.QueryRow(dataSql).Scan(&count)
	return count > 0
}

文章出處:基於gin的golang web開發:永遠不要相信使用者的輸入

相關文章