Go中的软件架构:Security - 防止SQL注入

200 阅读4分钟

Go中的软件架构。Security - 防止SQL注入

欢迎来到涵盖[质量属性/非功能要求]的系列的另一个帖子部分,这次我专门讨论了Security ,当与数据库一起工作时,防止SQL注入。


什么是SQL注入?

根据维基百科的说法。

SQL注入是一种代码注入技术,其中恶意的SQL语句被插入到一个条目字段中执行。

例如,考虑以下代码,意在执行一个DELETE 的SQL命令,在id 变量中接收一个字符串值。

1query := fmt.Sprintf("DELETE FROM users WHERE id = '%s'", id)
2
3if _, err := db.Exec(query); err != nil {
4	log.Fatalln("Couldn't delete", err)
5}

因此,如果id 碰巧是33ca99ce-f1d1-46c2-b26e-a0ff45011b18 ,那么与该值相匹配的记录将被删除,如预期的那样工作,但如果一个恶意的用户传入以下的值会发生什么?

"33ca99ce-f1d1-46c2-b26e-a0ff45011b18' OR ''='"

我们不希望发生的事情就会发生:我们将删除所有的记录!这是因为在SQL语句中的最后一句话是:"我们的记录将被删除。这是因为我们代码中的最后一条SQL语句将是以下内容。

DELETE FROM users WHERE id = '33ca99ce-f1d1-46c2-b26e-a0ff45011b18' OR ''=''

如果我们仔细观察,这个SQL语句的意思是。

删除任何与ID33ca99ce-f1d1-46c2-b26e-a0ff45011b18 相匹配的记录,或者如果' 等于'

所以换句话说:删除我们表中的每一条记录!因为' 总是要等于'

我们怎样才能防止SQL注入?

防止SQL注入的关键是,在建立SQL语句时,尽可能避免使用字符串连接,而倾向于使用相应的数据库引擎占位符。

例如,上面的代码应该被替换成下面这样的代码。

1// XXX: The placeholder value depends the database driver, some of them use
2// `?` instead of sequential $n values
3query := "DELETE FROM users WHERE id = $1"
4
5if _, err := db.Exec(query, id); err != nil {
6	log.Fatalln("Couldn't delete", err)
7}

在这种情况下,我们让数据库引擎处理参数,并处理预期字段的相应验证,所以如果我们传入一个像33ca99ce-f1d1-46c2-b26e-a0ff45011b18 的值,它将按预期工作,但如果我们尝试我们以前的讨厌的参数:'33ca99ce-f1d1-46c2-b26e-a0ff45011b18' OR ''='' ,它将不工作,给我们一个错误,如:

uuid类型的无效输入语法

那么,在使用动态SQL语句时呢?

这里的动态语句是指那些我们需要根据我们定义的不同条件参数动态建立的语句,例如,如果我们想通过不同的条件删除users ,我们仍然需要一种方法来动态创建这个查询,避免明确的字符串连接。

为了做这样的事情,我喜欢使用一个第三方软件包,叫做Masterminds/squirrel

[举例来说]。

 1psql := sq.Delete("users").Where("is_admin = ?", *isAdmin)
 2
 3if *birthYear > 0 {
 4	psql = psql.Where("birth_year > ?", *birthYear)
 5}
 6
 7sql, args, err := psql.PlaceholderFormat(sq.Dollar).ToSql()
 8if err != nil {
 9	log.Fatalln("Couldn't create SQL statement", err)
10}
11
12fmt.Println("query", sql, "args", args)
13
14//-
15
16stmt, err := db.PrepareContext(context.Background(), sql)
17if err != nil {
18	log.Fatalln("Couldn't prepare", err)
19}
20
21if _, err := stmt.ExecContext(context.Background(), args...); err != nil {
22	log.Fatalln("Couldn't delete", err)
23}
  • L1: 实例化查询
  • L3-5:添加条件,只删除那些birth_year 大于接收参数的内容
  • L7-10:我们生成最终的SQL语句
  • L16-23:我们执行它

总结

当使用关系型数据库时,我们总是建议使用准备好的语句和相应的引擎占位符,以避免出现像SQL注入这样的问题,在某些情况下,一些包(如pgx )会在背后缓存语句,这也给我们带来了一些性能上的改善。

如果需要创建动态SQL语句,可以考虑使用像squirrel 这样的包,也许可以使用具有明确权限的具体数据库用户,这样他们的访问范围就只受限于他们应该做的事情。