Go,Gorm 和 Mysql 是如何防止 SQL 注入的

Go,Gorm 和 Mysql 是如何防止 SQL 注入的

SQL 注入和 SQL 预编译技术

什么是 SQL 注入

所谓SQL注入(sql inject),就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。具体来说,它是利用现有应用程序,将(恶意的)SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。

SQL 注入例子

如下所示,是一个用户进行登录时,输入用户名和密码,再将数据通过表单传送到后端进行查询的 SQL 语句。

sql = "SELECT  USERNAME,PASSWORD FROM USER WHERE USERNAME='" + username + "' AND PASSWORD='" + password + "'";

上面这个 SQL 语句就存在 SQL 注入的安全漏洞。

假如 user 表中有用户名为 123456 ,密码为 123456 的记录,而在前台页面提交表单的时候用户输入的用户名和密码是随便输入的,这样当然是不能登录成功的。

但是如果后台处理的 SQL 语句是如上所写,前台页面用户名也是随便输入,而用户输入的密码是这样的 aaa' or '1'='1 ,处理登录的 SQL 语句就相当于是这样的:

SELECT USERNAME,PASSWORD FROM USER WHERE USERNAME='123456' AND PASSWORD='aaa' or '1'='1';

我们知道,1=1 是 true,所以上面这个 SQL 语句是可以执行成功的,这是一个 SQL 注入问题。

SQL 注入的解决

上述 SQL 注入问题产生的原因就是用户的输入是包含 SQL 语句的,而且后端执行 SQL 语句时直接将用户的输入和查询的 SQL 语句进行了拼接

因此,简单的拼接用户输入的数据和后端的查询 SQL 语句,是不可行的,我们需要将用户的输入作为一个完整的字符串,而忽略内部的 SQL 语句。当用户输入的密码是这样的 aaa’ or ‘1’='1 ,处理登录的 SQL 语句实际应该执行的是:

SELECT USERNAME,PASSWORD FROM USER WHERE USERNAME='123456' AND PASSWORD="aaa' or '1'='1";

这样就可以避免 SQL 注入导致的安全漏洞。

SQL 预编译技术

解决 SQL 注入问题的这个方案的关键要点实际上是将 SQL 语句和用户输入的查询数据分别进行处理,而不是一视同仁的作为 SQL 语句的不同部分进行拼接处理。在这个基础上,就产生了 SQL 预编译技术。

通常我们的一条 SQL 在 DB 接收到最终执行完毕返回可以分为下面三个过程:

  1. 词法和语义解析
  2. 优化 SQL 语句,制定执行计划
  3. 执行并返回结果

但是我们可以将其中需要用户输入的值用占位符替代,可以视为将 SQL 语句模板化或者说参数化,再将这样的 SQL 语句进行预编译的处理,在实际运行的时候,再传入用户输入的数据。

使用这样的 SQL 预编译技术,除了可以防止 SQL 注入外,还可以对预编译的 SQL 语句进行缓存,之后的运行就省去了解析优化 SQL 语句的过程,可以加速 SQL 的查询

Gorm 和 Go 端的 SQL 预编译

在 Gorm 中,就为我们封装了 SQL 预编译技术,可以供我们使用。

db = db.Where("merchant_id = ?", merchantId)

在执行这样的语句的时候实际上我们就用到了 SQL 预编译技术,其中预编译的 SQL 语句merchant_id = ?和 SQL 查询的数据merchantId将被分开传输至 DB 后端进行处理。

db = db.Where(fmt.Sprintf("merchant_id = %s", merchantId))

而当你使用这种写法时,即表示 SQL 由用户来进行拼装,而不使用预编译技术,随之可能带来的,就是 SQL 注入的风险。

Gorm 端的 SQL 预编译

// SQLCommon is the minimal database connection functionality gorm requires.  Implemented by *sql.DB.
type SQLCommon interface {
   Exec(query string, args ...interface{}) (sql.Result, error)
   ......
}

Go 端的 SQL 预编译

// src/database/sql/sql.go
func (db *DB) execDC(ctx context.Context, dc *driverConn, release func(error), query string, args []interface{}) (res Result, err error) {
    ......
      resi, err = ctxDriverExec(ctx, execerCtx, execer, query, nvdargs)
    ......
      if err != driver.ErrSkip {
    ......
         return driverResult{dc, resi}, nil
      }
    ......
      si, err = ctxDriverPrepare(ctx, dc.ci, query)
    ......
   ds := &driverStmt{Locker: dc, si: si}
    ......
   return resultFromStatement(ctx, dc.ci, ds, args...)
}

实际的实现最终还是落到了go-sql-driver上,如下面代码所示go-sql-driver支持开启预编译和关闭预编译,由mc.cfg.InterpolateParams = false、true决定,可以看出gorm中mc.cfg.InterpolateParams = true,即开启了预编译

func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
    ......
   if len(args) != 0 {
      if !mc.cfg.InterpolateParams {
         return nil, driver.ErrSkip
      }
      prepared, err := mc.interpolateParams(query, args)
      if err != nil {
         return nil, err
      }
      query = prepared
   }
    ......
   err := mc.exec(query)
    ......
  return nil, mc.markBadConn(err)
}

Mysql 端的SQL 预编译

在MySQL中是如何实现预编译的,MySQL在4.1后支持了预编译,其中涉及预编译的指令实例如下

可以通过PREPARE预编译指令,SET传入数据,通过EXECUTE执行命令

mysql> PREPARE stmt1 FROM 'SELECT SQRT(POW(?,2) + POW(?,2)) AS hypotenuse';
Query OK, 0 rows affected (0.00 sec)
Statement prepared

mysql> SET @a = 3;
Query OK, 0 rows affected (0.00 sec)

mysql> SET @b = 4;                                                   
Query OK, 0 rows affected (0.00 sec)

mysql> EXECUTE stmt1 USING @a, @b;
+------------+
| hypotenuse |
+------------+
|          5 |
+------------+
1 row in set (0.00 sec)

mysql> DEALLOCATE PREPARE stmt1;                                     
Query OK, 0 rows affected (0.00 sec)

首先我们先简单回顾下客户端使用 Prepare 请求过程:

  1. 客户端发起 Prepare 命令将带 “?” 参数占位符的 SQL 语句发送到数据库,成功后返回 stmtID。
  2. 具体执行 SQL 时,客户端使用之前返回的 stmtID,并带上请求参数发起 Execute 命令来执行 SQL。
  3. 不再需要 Prepare 的语句时,关闭 stmtID 对应的 Prepare 语句。

这里展示不使用 sql 预编译和使用 sql 预编译时的 Mysql 的日志。

2020-06-30T08:14:02.430089Z           10 Query        COMMIT
2020-06-30T08:14:02.432995Z           10 Query        select * from user where merchant_id='123456'

2020-06-30T08:15:10.581287Z           12 Query        COMMIT
2020-06-30T08:15:10.584109Z           12 Prepare        select * from user where merchant_id =?
2020-06-30T08:15:10.584725Z           12 Execute        select * from user where merchant_id ='123456'
以下是使用Golang GORM进行MySQL递归查询单表的示例代码: ```go package main import ( "fmt" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" ) type Category struct { ID int Name string ParentID int Children []Category `gorm:"foreignkey:ParentID"` } func main() { db, err := gorm.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database?charset=utf8mb4&parseTime=True&loc=Local") if err != nil { panic(err) } defer db.Close() var categories []Category db.Where("parent_id = ?", 0).Preload("Children").Find(&categories) for _, category := range categories { fmt.Println(category.Name) for _, child := range category.Children { fmt.Println(" ", child.Name) } } } ``` 在这个示例中,我们定义了一个Category结构体,其中包含ID、Name、ParentID和Children字段。Children字段是一个Category类型的切片,用于存储子类别。在结构体中,我们使用了GORM的foreignkey标记来指定ParentID字段是外键,Children字段是通过ParentID字段与Category表关联的。 在main函数中,我们首先使用GORM的Open函数打开MySQL数据库连接。然后,我们定义了一个categories切片,用于存储查询结果。我们使用GORM的Where函数指定ParentID为0,即查询所有顶级类别。然后,我们使用GORM的Preload函数预加载Children字段,以便在查询结果中包含子类别。最后,我们使用GORM的Find函数执行查询,并将结果存储在categories切片中。 最后,我们遍历categories切片,并打印每个类别及其子类别的名称。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值