预编译防止百分之95的SQL注入

admin 2022年1月6日01:43:42评论214 views字数 6309阅读21分1秒阅读模式

数据库预编译起源

(1)数据库SQL语句编译特性:
数据库接受到sql语句之后,需要词法和语义解析,优化sql语句,制定执行计划。这需要花费一些时间。但是很多情况,我们的一条sql语句可能会反复执行,或者每次执行的时候只有个别的值不同(比如query的where子句值不同,update的set子句值不同,insert的values值不同)。
(2)减少编译的方法
如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则效率就明显不行了。为了解决上面的问题,于是就有了预编译,预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化。一次编译、多次运行,省去了解析优化等过程。
(3)缓存预编译
预编译语句被DB的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中(相当于一个涵数)就会得到执行。
并不是所以预编译语句都一定会被缓存,数据库本身会用一种策略(内部机制)。
(4) 预编译的实现方法
预编译是通过PreparedStatement和占位符来实现的。

预编译作用

  • 预编译阶段可以优化 sql 的执行
    预编译之后的 sql 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的sql,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。可以提升性能。
  • 防止SQL注入
    使用预编译,而其后注入的参数将不会再进行SQL编译。也就是说其后注入进来的参数系统将不会认为它会是一条SQL语句,而默认其是一个参数,参数中的or或者and 等就不是SQL语法保留字了。

如何开启预编译

(1)数据库是否默认开启预编译和JDBC版本有关。
也可以配置jdbc链接时强制开启预编译和缓存:useServerPrepStmts和cachePrepStmts参数。预编译和预编译缓存一定要同时开启或同时关闭。否则会影响执行效率

1
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true&cachePrepStmts=true");  

(2)mysql的预编译

  • 开启了预编译缓存后,connection之间,预编译的结果是独立的,是无法共享的,一个connection无法得到另外一个connection的预编译缓存结果。
  • 经过试验,mysql的预编译功能对性能影响不大,但在jdbc中使用PreparedStatement是必要的,可以有效地防止sql注入。
  • 相同PreparedStatement的对象 ,可以不用开启预编译缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
          conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true");  
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, "aaa");
ResultSet rs1 = stmt.executeQuery();//第一次执行
s1.close();
stmt.setString(1, "ddd");
ResultSet rs2 = stmt.executeQuery();//第二次执行
rs2.close();
stmt.close();
//查看mysql日志
1 Prepare select * from users where name = ?
1 Execute select * from users where name = 'aaa'
1 Execute select * from users where name = 'ddd'

mybatis是如何实现预编译的

mybatis 默认情况下,将对所有的 sql 进行预编译。mybatis底层使用PreparedStatement,过程是先将带有占位符(即”?”)的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,然后直接执行带有真实参数的sql。核心是通过#{ } 实现的
在预编译之前,#{ } 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符?。

1
2
3
4
//sqlMap 中如下的 sql 语句
select * from user where name = #{name};
//解析成为预编译语句
select * from user where name = ?;

如果${ },SQL 解析阶段将会进行变量替换。不能实现预编译。

1
2
3
select * from user where name = '${name}'
//传递的参数为 "ruhua" 时,解析为如下,然后发送数据库服务器进行编译。
select * from user where name = "ruhua";

JDBC预编译过程

两个重要参数

1
useServerPrepStmts=true&&cachePrepStmts=true

首先来看不加cachePrepStmts=true参数的情况,如下:

JDBC中原url配置语句**

1
url=jdbc:mysql://127.0.0.1:3306/mjduan?useUnicode=true&characterEncoding=utf-8&useSSL=false

服务器端MySQL的general日志里面内容如下:

​ 执行的三条语句被打印出来了

1
2
3
4
5
6
2018-06-12T23:37:54.142048Z	   10 Query	SET NAMES utf8
2018-06-12T23:37:54.143931Z 10 Query SET character_set_results = NULL
2018-06-12T23:37:54.145705Z 10 Query SET autocommit=1
2018-06-12T23:37:54.210026Z 10 Query SELECT id,name,age FROM student WHERE id = 1
2018-06-12T23:37:54.236635Z 10 Query SELECT id,name,age FROM student WHERE id = 1
2018-06-12T23:37:54.239985Z 10 Query SELECT id,name,age FROM student WHERE id = 1

JDBC连接的url中添加useServerPrepStmts=true**

1
url=jdbc:mysql://127.0.0.1:3306/mjduan?useUnicode=true&characterEncoding=utf-8&useSSL=false&&useServerPrepStmts=true

在如下日志中,每个Ececute之前都有一个Prepare,说明执行SQL之前,都对该SQL进行解析、优化了,所以没有起到预编译的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
2018-06-12T23:29:34.175252Z	    5 Query	SET NAMES utf8
2018-06-12T23:29:34.176954Z 5 Query SET character_set_results = NULL
2018-06-12T23:29:34.178768Z 5 Query SET autocommit=1
2018-06-12T23:29:34.214910Z 5 Prepare SELECT id,name,age FROM student WHERE id = ?
2018-06-12T23:29:34.244389Z 5 Execute SELECT id,name,age FROM student WHERE id = 1
2018-06-12T23:29:34.265083Z 5 Close stmt
2018-06-12T23:29:34.266680Z 5 Prepare INSERT INTO student(name,age) VALUES(?, ?)
2018-06-12T23:29:34.268304Z 5 Query select @@session.tx_read_only
2018-06-12T23:29:34.275772Z 5 Execute INSERT INTO student(name,age) VALUES('明明', 20)
2018-06-12T23:29:34.283289Z 5 Close stmt
2018-06-12T23:29:34.284142Z 5 Prepare SELECT id,name,age FROM student WHERE id = ?
2018-06-12T23:29:34.285013Z 5 Execute SELECT id,name,age FROM student WHERE id = 1
2018-06-12T23:29:34.287157Z 5 Close stmt

**JDBC连接的url加上useServerPrepStmts=true&cachePrepStmts=true**

1
url=jdbc:mysql://127.0.0.1:3306/mjduan?useUnicode=true&characterEncoding=utf-8&useSSL=false&&useServerPrepStmts=true&cachePrepStmts=true

再去看MySQL的general日志,如下,这次生效了。

执行三条一样的语句,只有一次Prepare

1
2
3
4
5
6
7
2018-06-12T23:44:03.197228Z	   11 Query	SET NAMES utf8
2018-06-12T23:44:03.202524Z 11 Query SET character_set_results = NULL
2018-06-12T23:44:03.206439Z 11 Query SET autocommit=1
2018-06-12T23:44:03.310079Z 11 Prepare SELECT id,name,age FROM student WHERE id = ?
2018-06-12T23:44:03.378740Z 11 Execute SELECT id,name,age FROM student WHERE id = 1
2018-06-12T23:44:03.427651Z 11 Execute SELECT id,name,age FROM student WHERE id = 1
2018-06-12T23:44:03.431207Z 11 Execute SELECT id,name,age FROM student WHERE id = 1

小总结

1、jdbc链接时强制开启预编译和缓存:useServerPrepStmts和cachePrepStmts参数。预编译和预编译缓存一定要同时开启或同时关闭。否则会影响执行效率。

2、 预编译功能只是在同一个Connection中生效,一个Connection无法获取另一个Connection中的预编译结果。

3、使用Mybatis时,预编译功能只在同一个SqlSession中生效?这种说法不对,俩个SqlSession有可能拿到的是同一个Connection(因此DataSource),这个时候这俩个SqlSession就能共享该Connection中的预编译结果。

4、MyBatis是对JDBC的封装,MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架。

预编译只能防止百分之95的sql注入

为什么预编译只能防95%

防sql注入都用参数化的方法,但是有些地方是不能参数化的。

比如order by后就不能参数化,一般挖sql注入时找有排序功能需求的位置(比如博客常按时间排序),基本十之六七都能挖到sql注入。

典型的java写的sql执行代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
Connection conn = DBConnect.getConnection();
PreparedStatement ps = null;
ResultSet rs=null;

String sql = " SELECT passwd FROM test_table1 WHERE username = ? ";

ps = conn.prepareStatement(sql);
# 通过setString()指明该参数是字符串类型
ps.setString(1, username);
# 另外还有setInt()等一些其他方法
# ps.setInt(2, test_param);
rs = ps.executeQuery();

ps.setString(1, username)会自动给值加上引号。比如假设username=“ls”,那么拼凑成的语句会是String sql = “ SELECT passwd FROM test_table1 WHERE username = ‘ls’ “;

再看order by,order by后一般是接字段名,而字段名是不能带引号的,比如 order by username;如果带上引号成了order by ‘username’,那username就是一个字符串不是字段名了,这就产生了语法错误。

所以order by后不能参数化的本质是:一方面预编译又只有自动加引号的setString()方法,没有不加引号的方法;而另一方面order by后接的字段名不能有引号。(至于为什么不弄个能不自动加引号的set方法那就不太懂了)

更本质的说法是:不只order by,凡是字符串但又不能加引号的位置都不能参数化;包括sql关键字、库名表名字段名函数名等等。

不能被参数化防止sql注入方法

不能参数化的位置,不管怎么拼接,最终都是和使用“+”号拼接字符串的功效一样:拼成了sql语句但没有防sql注入的效果。

但好在的一点是,不管是sql关键字,还是库名表名字段名函数名对于后台开发者来说他的集合都是有限的,更准确点应该说也就那么几个。

这时我们应可以使用白名单的这种针对有限集合最常用的处理办法进行处理,如果传来的参数不在白名单列表中,直接返回错误即可。

代码类似如下:

1
2
3
4
5
6
7
8
9
if para_str.equals("key_str1"){
...;
}
else if test_str.equals("key_str2"){
...;
}
else{
throw new Exception("parameter error.");
}

关键词

1、SQLite数据库不支持预编译

2、能否预编译是数据库的特性,由后台配置决定是否预编译。

3、 预编译是通过PreparedStatement和占位符来实现的

4、一般挖sql注入时找有排序功能需求的位置(比如博客常按时间排序),使用的是order by语句,不可以被预编译。

5、不只order by,凡是字符串但又不能加引号的位置都不能参数化;包括sql关键字、库名表名字段名函数名等等。

https://www.jianshu.com/p/9972d7b33061

https://my.oschina.net/u/2518341/blog/1830366

https://www.cnblogs.com/lsdb/p/12084038.html

FROM :b0urne.top | Author:b0urne

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:43:42
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   预编译防止百分之95的SQL注入https://cn-sec.com/archives/722435.html

发表评论

匿名网友 填写信息