
Go 是一门很简洁的语言,它有很多有趣的特性,例如 Go 语言从语法层面进行了以下限定:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。
但是偶尔也遇到一些非常反直觉的「问题?」。以下便引出我今天想讨论的话题:震惊!Gin 框架 ShouldBind 使用 JSON 的巨大隐患。而且 不止 JSON, 这个隐患在 XML 上同样存在。
我的建议:Gin 框架下,不要轻易使用 ShouldBind, 而是应使用 ShouldBindWith 替代
常写 Gin 的朋友都知道,以下是一段比较常规,用于参数绑定 Controller 层代码:
// script 1
// 绑定 JSON
type LoginParam struct {
User string `from:"user" binding:"required"`
Password string `from:"password" binding:"required"`
}
// OnInit 初始函数
func (p *LoginParam) OnInit(ctx context.Context, req ghttp.Request, next frame.MiddleWareQueue) (data interface{}, err error) {
c := gin.Context{
Request: req.HTTPRequest(),
}
if err := c.ShouldBind(&d); err != nil {
return nil, errors.New("bind params error")
}
return next.Next(ctx, req)
}
// OnExecute 业务执行函数
func (p *LoginParam) OnExecute(ctx context.Context, req ghttp.Request) (interface{}, error) {
// todo 业务逻辑
}
看起来没有什么问题是吧。因为绑定了 struct LoginParam 的指针 p 是一个全局变量,使用起来非常方便。那当程序执行过程中想加入其它全局变量多时候,就有聪明的小伙伴会想到,将其直接放到 LoginParam 里是不是就很方便,只要不给他打 JSON tag 标签,不就可以了嘛,非常符合直觉,例子代码如下:
// script 2
// 绑定 FROM
type LoginParam struct {
User string `from:"user" binding:"required"`
Password string `from:"password" binding:"required"`
UserID int `from:"-"`
}
// OnInit 初始函数
func (p *LoginParam) OnInit(ctx context.Context, req ghttp.Request, next frame.MiddleWareQueue) (data interface{}, err error) {
c := gin.Context{
Request: req.HTTPRequest(),
}
if err := c.ShouldBind(&d); err != nil {
return nil, errors.New("bind params error")
}
return next.Next(ctx, req)
}
// OnExecute 业务执行函数
func (p *LoginParam) OnExecute(ctx context.Context, req ghttp.Request) (interface{}, error) {
// 判断用户登录 给 UserID 赋值
if isLogin {
p.UserID = 1024
}
// todo 业务逻辑
}
🤔 问:3s 思考一下,这段代码有问题吗? 😂 答:有!有巨大问题!一个安全漏洞被你成功创建了!
如果这个时候,不法分子通过 JSON 或者 XML header 方式请求接口,并传入参数例如: {"user":"123", "password":"xxx", "UserID":999}
或者 {"user":"123", "password":"xxx", "userID":888}
p.UserID 都将被 ShouldBind 初始化赋值。非常反直觉是不是,我并没有给 UserID 加 from:"userId"
标签,甚至加了不初始化的 from:"-"
标签,而且 userID 居然也能初始化 p.UserID
这个是因为,在使用 ShouldBind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定。如果你明确知道要绑定什么,可以使用 MustBindWith 或 ShouldBindWith。
如果 Gin 推断入参为 JSON 类型,会调用 encoding/json 包对参数进行绑定。而 Go 的 JSON 包,遵循任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。UserID,被当作了可以对外暴露的 Public 变量,并且 JSON 又似乎有一套,默认映射机制,无需强制加 JSON tag 标签,自动完成了参数绑定。
丰富的非 Go 语言开发者,用正常的思维推理,你能想到这里有坑吗 😄
所以我建议两点:
- 明确输入参数类型,其它均为非法。使用 ShouldBindWith 而不是 ShouldBind
- ShouldBind 绑定的结构体里面,不应该放置请求参数之外的变量。
一段简单的代码如下:
// script 3
// 绑定 FROM
type LoginParam struct {
req struct LoginReq
UserID int
}
// 绑定 FROM
type LoginReq struct {
User string `from:"user" binding:"required"`
Password string `from:"password" binding:"required"`
}
// OnInit 初始函数
func (p *LoginParam) OnInit(ctx context.Context, req ghttp.Request, next frame.MiddleWareQueue) (data interface{}, err error) {
c := gin.Context{
Request: req.HTTPRequest(),
}
if err := c.ShouldBindWith(&d.param, binding.Form); err != nil {
return nil, errors.New("bind params error")
}
return next.Next(ctx, req)
}
// OnExecute 业务执行函数
func (p *LoginParam) OnExecute(ctx context.Context, req ghttp.Request) (interface{}, error) {
// 判断用户登录 给 UserID 赋值
if isLogin {
p.UserID = 1024
}
// todo 业务逻辑
}
参考资料: