一、关于GORM
Go有诸多的ORM框架,GORM是Golang语言中一款性能极好的ORM库,对开发人员相对是比较友好的。当然原生库、xorm等也是比较出名的,感兴趣的也可以看看这些,但使用场景最多的还属gorm。
Gorm功能概览
- 全功能ORM(几乎)
- 关联(包含一个,包含多个,属于,多对多,多种包含)
- Callbacks(创建/保存/更新/删除/查找之前/之后)
- 预加载(急加载)
- 事务
- 复合主键
- SQL Builder
- 自动迁移
- 日志
- 可扩展,编写基于GORM回调的插件
- 每个功能都有测试
- 开发人员友好
安装十分简单,只需要一条go get
**命令即可。
go get -u github.com/jinzhu/gorm
下面是一个简单的demo,这里以mysql为例,方便快速了解上手。
go
package main
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"log"
)
// UserInfo record model
type UserInfo struct {
Id int `gorm:"column:id"`
Age int `gorm:"column:age"`
Name string `gorm:"column:name"`
Address string `gorm:"column:address"`
Content string `gorm:"column:content"`
}
// TableName 设置User的表名为`userinfo`, 默认会是userinfos
func (UserInfo) TableName() string {
return "userinfo"
}
func main() {
db, err := gorm.Open("mysql", "root:passwd@(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.LogMode(true)
u := new(UserInfo)
uinfos := make([]*UserInfo, 1)
db.Where("age = ?", 22).Find(u)
log.Println(u)
db.Where("age > ?", 10).Find(&uinfos)
log.Println(uinfos)
}
Gorm支持好多种数据库,下面是简单的连接例子:
要连接到数据库首先要导入驱动程序。例如
import _ "github.com/go-sql-driver/mysql"
为了方便记住导入路径,GORM包装了一些驱动。
import _ "github.com/jinzhu/gorm/dialects/mysql"
// import _ "github.com/jinzhu/gorm/dialects/postgres"
// import _ "github.com/jinzhu/gorm/dialects/sqlite"
// import _ "github.com/jinzhu/gorm/dialects/mssql"
- MySQL
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func main() {
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
defer db.Close()
}
- PostgreSQL
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)
func main() {
db, err := gorm.Open("postgres", "host=myhost user=gorm dbname=gorm sslmode=disable password=mypassword")
defer db.Close()
}
- Sqlite3
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
func main() {
db, err := gorm.Open("sqlite3", "/tmp/gorm.db")
defer db.Close()
}
二、常见SQL注入场景
SQL注入主要是Web应用未对用户输入进行合法校验,最终用户输入走向了查询语句,导致攻击者能在Web应用预先写好的SQL语句中执行了额外的SQL语句,造成非授权的任意查询,进而导致数据库信息泄露,有时甚至可以反弹shell。
为了防止注入的存在,gorm框架贴心的对大部分场景都进行了预编译处理,但由于研发安全意识的缺乏以及框架本身的局限,基于gorm的Web应用中还是存在着大量的SQL注入问题。
2.1 配置/连接数据库
DB是数据库操作中最核心的结构,后续的CRUD操作基本上都基于该结构对象。
go
type DB struct {
*Config
Error error
RowsAffected int64
Statement *Statement
clone int
}
配置数据库
go
func main(){
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic("Connection error")
}
defer db.Close()
// 后续的CRUD操作基于db
// 比如db.Where(), db.Like(), db.Order()等
}
2.2 审计
下面来介绍审计过程中常遇见的存在问题的场景,这也是我们最关心的部分。
首先明确一个问题,什么时候会存在SQL注入?当CRUD操作**直接拼接了用户输入*,并且未做有效过滤/防护时,就会存在注入。那么什么是拼接用户输入呢?—— +, fmt.Sprintf(), buf.WriteString()
等字符串的连接,均可视为拼接***。
默认情况下,GORM对结构体、map结构的value在框架底层都进行了预编译,所以使用此类方式进行CRUD操作时是十分安全的,无须担心SQL注入问题,尤其结构体在增删改操作中使用的很多。
- where查询
该方法是很常见的条件查询,是后续许多查询的基础。
go
func (s *DB) Where(query interface{}, args ...interface{}) *DB
以下类型的代码存在注入:
go
// name可控
db.Where("name = '" + name + "'")
sql = fmt.Sprintf("name = '%s'", name)
db.Where(sql)
以下类型的代码则是安全的:
go
// name可控,但使用了预编译,不存在注入问题
db.Where("name = ?", name)
* map的key
GORM对map的value进行了预编译,但却没对key进行预编译,此时极其容易审到注入。如下代码就存在SQL注入:
go
// name_key可控,key框架未做预编译,代码也未作处理,存在注入
m1 := map[string]interface{} {
name_key: "tom",
age: 6,
}
db.Where(m1)
// name_value可控,框架底层对value做了预编译,不存在注入
m2 := map[string]interface{} {
"name": name_value,
"age": 6,
}
db.Where(m2)
那么GORM为何没有对key做处理呢?原因在于map的key等价于sql语句中的列名,众所周知列名是无法执行预编译操作的,审计时可以重点关注这部分。
* Raw查询
go
// 使用原始的sql作为查询条件
func (s *DB) Raw(sql string, values ...interface{}) *DB
Raw是GORM提议提供的中较为宽松的写法,能让研发自由的写原生SQL语句,例如类似desc tables
的语句,显然无法通过使用Where()
、Order()
等函数执行。由于其较高的自由度,该函数颇受欢迎,此外,许多开发者对该函数存在一定误解,并不知道该函数也能使用预编译,由此引入了诸多注入问题。
go
// req用户可控,此类写法存在注入
sql := fmt.Sprintf("select id, %s.age from users where name = %s order by 1 desc", req.u, req.name)
db.Raw(sql)
* Exec查询
go
func (s *DB) Exec(sql string, values ...interface{}) *DB
Exec和Raw函数情况类似,这里不再赘述。
* orderby查询
采用预编译执行SQL语句传入的参数不能作为SQL语句的一部分,那么OrderBy所代表的的列名、或者是后面跟随的ASC/DESC也无法进行预编译处理。由于框架缺乏对此种类型的安全处理,加之许多开发者并不清楚SQL注入,Order()
就成了SQL注入的高发地带。
go
func (s *DB) Order(value interface{}, reorder ...bool) *DB
req.loc用户可控,代码中未对loc做任何处理,存在注入风险
go
db.Order(req.loc)
req.loc用户可控,代码中虽做了检查,但很简单就可以绕过,仍存在注入问题
go
if strings.Contains(req.loc, "shanghai") {
db.Order(req.loc)
}
req.loc用户可控,代码中通过map做了白名单校验,无法绕过,不存在注入问题
go
validateCols := map[string]bool{"col1": true, "col2":true}
if _, ok := validateCols[req.loc]; !ok {
fmt.Println("列名不合法")
return
}
db.Order(req.loc)
* groupby查询
groupby和orderby的问题类似,这里不再赘述。
* like模糊查询
like一般是基于where的查询,许多开发者对此处存在误区,很喜欢下面这类写法。
req.music用户可控,直接拼接了用户输入,存在注入问题。
go
cond := "music like %"+req.music+"%"
db.Where(cond)
req.music用户可控,使用了预编译,不存在注入问题。
go
db.Where("musick like %?%", req.music)
* in范围查询
in是基于where的查询,存在的问题和like类似。
req.name用户可控,直接拼接了用户输入,存在注入问题。
go
cond := "name in (" + req.name1 + req.name2 + ")"
db.Where(cond)
req.name用户可控,使用了预编译,不存在注入问题。
go
db.Where("name in (?)", []string{req.name1, req.name2})
总而言之,会导致注入问题的,只有字符串类型,map结构的key;而结构体、map结构的value、数字类型等,均不会存在注入问题。
三、防御建议
一个大的原则是,优先使用预编译,无法使用预编译的,使用白名单/转义操作进行防御。
- 常规值的拼接
对于除表/列名的常规值,使用参数化查询对语句进行预编译,可以完全防御SQL注入。 - 表/列名的拼接
对于表/列名,无法使用预编译的,则可使用白名单机制,或者对特殊字符进行转义来防御注入。
四、参考资料
一、 前言 XSS是最为常见的Web漏洞之一,多年来连续入选OWASP TOP 10,相信大家都耳熟能详。 它是一种代码注入类的攻击,是一种客户端侧的攻击,攻击者通过在Web应用中注入恶意JavaScript代码,通过点击URL,最终在受害者浏览器端执行的一种…
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论