学习SQL注入引发的思考

学习SQL注入引发的思考

1.序言

因为公司项目的上个版本已经进入预发布阶段,所以最近闲下来了,就觉得应该找点事情做,刚好上个版本上测试的时候,有个测试的同事提出了SQL注入这个问题,所以利用这个时间来了解一下SQL注入。
首先声明一下,我自己是一个刚工作不到一年的小白,所以以下是我自己在学习过程中总结的,所以都是使用比较通俗易懂的白话,没有很专业介绍,所以如果是想看有关SQL注入专业的介绍的话,可以去看更专业的文章,谢谢。

2.SQL注入的原理

其实SQL注入的原理非常简单,下面以几个非常简单的例子说明。

2.1 Statement

     /**
       * 主要功能:
       * 1.测试Statement和PrepareStatement的区别
       * 2.模拟sql注入
       * */
      // statement用来执行SQL语句
      Statement statement = conn.createStatement();

      // 要执行的SQL语句
      String sql = "SELECT * FROM B_USER_ROLE_MAPPING WHERE hrole_code='"+roleCode+"'";

      System.out.println("执行的sql语句:"+sql);
      // 结果集
      ResultSet rs = statement.executeQuery(sql);

      System.out.println("-----------------");
      System.out.println("执行结果如下所示:");
这个例子采用了最原始的JDBC的方式连接数据库,然后使用Statement拼接sql语句
    mysqlDao.conn("HUIDA_OPERATOR");
然后在调用方法的时候传入一个参数,这个参数就是用来拼接上面的SQL语句。执行方法可以看到以下结果:

这里写图片描述

这就是一个正常的传入参数,拼接SQL,执行SQL的过程,然后我们将传入的参数做一点小小的改动,如下图所示:
    mysqlDao.conn("HUIDA_OPERATOR' OR '1'='1");
再来看看执行结果:

这里写图片描述

现在可以看到,因为参数中增加了OR ‘1’=’1,这个语句在SQL执行时将会认为是永真的,所以这个SQL语句查出了数据库中所有的值。这就是 SQL注入
SQL注入 其实就是利用SQL一些语法上的花样从而去改变一个SQL的执行结果。比如上一个语句,我让它拼接上一个永真的判断条件,从而使得原本的条件失效。
那么,如果是这样一个SQL语句

SELECT * FROM USER WHERE username='"+username+"' AND password='"+password+"'"

然后我将username参数输入为:123' OR 1=1 #
最后我这个SQL语句就变成

SELECT * FROM USER WHERE username='123' OR 1=1 #' AND password='"+password+"'"

大家应该都知道,在SQL的语法中’#’符号是注释,也就说’#’符号之后的都会注释,那么这个SQL就变成无论我输入的用户名是多少,无论我输入的密码是多少,这个语句都可以查询到结果,这样就可以跳过验证的步骤。甚至,可以采用SQL注入的方式,注释掉原本的SQL,然后加上删除操作,可以让系统瘫痪。当然这只是为了强调SQL注入的高风险!!!

2.2 PrepareStatement

当我明白SQL注入之后,我开始在我们公司的网站上进行了一些SQL注入的尝试,但是我发现无论我输入什么样的参数我都没办法完成SQL注入,因为项目中采用的是Mybatis,而Mybatis就是将所有的SQL语句写在配置文件中,然后传入参数,所以我开始去深入了解Mybatis底层的原理,发现Mybatis采用的是PrepareStatement,它的原理是在SQL语句执行之前先进行了一次预处理,然后再将参数经过一定的处理后,拼接成完整的SQL,最后才执行。
因为提到了一个新的概念,预处理,所以先来了解一下它的意思,在MySQL中,执行一条SQL语句时,会先根据SQL,是否能够使用索引等分析出一条最优的查询策略,然后按照这个策略查询结果。预处理 就是先分析出一个最优的查询策略,然后将这个策略保存起来,当调用这个预处理语句进行查询时,直接按照策略查询出结果就好。这也是很多项目在查询的数据量很大时,采用预处理方式的原因。
mysql>PREPARE myfun FROM 'select * from B_USER where huser_code=?';
mysql>set @str="wang";
mysql>EXECUTE myfun USING @str;
看上面这个脚本,第一个语句就将SQL语句进行了预处理,然后设置了一个变量,最后将这个使用这个变量执行SQL语句。
当执行了上面的脚本之后,使用下面的语句就可以查询到现在数据库中是否有预处理的语句:
show status like '%prepare%'

这里写图片描述

Prepared_stmt_count就是预处理语句的数量,现在变成了1。
接下来,当然就是在程序中去使用PrepareStatement语句来尝试一下了。
     String sql = "SELECT * FROM B_USER_ROLE_MAPPING WHERE hrole_code=?";
     //PrepareStatement用来执行SQL语句
      PreparedStatement statement2=conn.prepareStatement(sql);

      statement2.setString(1, roleCode);

      // 结果集
      ResultSet rs = statement2.executeQuery();
将之前的语句改为采用PrepaStatement的方式,你会发现sql语句变成了上面预处理语句的格式,即参数采用‘?‘符号才代替,就是一个占位符,然后在设置参数,最后执行语句。
当你运行Main方法之后,你再执行上面查看预处理语句数量的方法,你会发现数量还是1,并没有增加一条!!!
这里存在两个问题:

①使用JDBC驱动连接MYSQL,默认是不开启预处理的。
②预处理语句的生存周期是在一个连接中的,即当客户端断开连接之后这条预处理语句就消失了。

针对以上两个问题,程序要做如下改动:
String url = "jdbc:mysql://localhost:3306/gaia?useServerPrepStmts=true";
URL中要加上useServerPreStmts=true用来开启预处理语句。
    MysqlDAO mysqlDao=new MysqlDAO();
    mysqlDao.conn("HUIDA_OPERATOR' OR '1'='1");

    while(true){

    }
加上一个永久循环,除了手动关闭程序,让这个连接一直保存着。
现在再执行main方法,就会发现预处理语句变成了2!!!
但是到这里我们还是没有找到PrepareStatement防止SQL注入的原因,所以我们需要查询MySQL的日志,查看整个SQL语句的执行过程。
(关于MySQL的日志介绍在我的另一片博客中,有兴趣的可以去看看。)
在MySQL中有一个日志是用来查看所有的SQL执行过程的,即general_log,但是MySQL默认是没有开启这个日志的,所以需要手动开启,windows环境下修改mysql的默认启动文件my.ini,如下
# windows:my.ini
# 通用查询日志输出格式
log-output=FILE 
# 是否开启通用查询日志
general-log=1
# 通用查询日志输出位置
general_log_file="USER-20160621RL.log"
然后重启MySQL使配置生效,然后打开日志文件,再执行一次上面的程序,然后就会看到如下日志:

这里写图片描述

从日志中,就可以很清楚的看到,采用JDBC连接MySQL之后,显示对语句进行了预处理,然后才执行了SQL语句,而且将参数中的每一个单引号都进行了转义,这也就是PrepareStatement防止SQL注入的原理。
如果有兴趣查看PrepareStatement的源码就会发现,它在设置参数时,会在所有的参数开始和结尾都加上一个单引号,然后会遍历整个字符串,对每一个特殊符号都会进行转义的处理。
在明白了PrepareStatement防止SQL注入的原理之后,我又将程序修改了一下:
    String sql = "SELECT * FROM B_USER_ROLE_MAPPING WHERE hrole_code=?";

    //PrepareStatement用来执行SQL语句
      PreparedStatement statement1=conn.prepareStatement(sql);

      statement1.setString(1, roleCode);

      // 结果集
      ResultSet rs = statement1.executeQuery();

      //PrepareStatement用来执行SQL语句
      statement1=conn.prepareStatement(sql);

      statement1.setString(1, roleCode);

      // 结果集
      rs = statement1.executeQuery();
再执行程序,然后查看日志:

这里写图片描述

竟然有两个预处理语句,不是说会预处理后会将语句保存起来吗,然后就直接根据参数查询结果吗?
在查询文档后,发现默认是不会将预处理语句缓存的,需要修改url如下:
String url = "jdbc:mysql://localhost:3306/gaia?useServerPrepStmts=true&cachePrepStmts=true";
cachePrepStmts=true就是用来缓存预处理语句的,然后再执行程序,查看日志:

这里写图片描述

!!!竟然还是两次预处理!!!讲真,到这里我是有点懵的,然后通过各种尝试,我发现了问题所在:
statement1.close();
我在第一个查询完成后没有关闭,又进行了第二次预处理,于是出现了两次,如果我在第一次预处理之后,将其关闭,再进行预处理,就会使用缓存中已经存在的预处理语句。再查看日志:

这里写图片描述

终于看到了想要的结果!!!
如果你正打算尝试整个过程的话,除了以上几点需要注意,还有一点,就是两个sql要完全一模一样,相当于key值,一旦sql有一点不一样,即使是多了一个空格,都会被当做两个预处理语句,所以在程序中使用的时候,最好是将SQL语句定义为一个变量。mybatis的底层就是采用PrepareStatement,然后将所有的sql集中在配置文件中。#{value}就相当于占位符,但是${value}就是简单的拼接,所以为了防止SQL注入,尽量使用‘#’,但是具体情况还是要具体分析。
最后还有一个尝试,毕竟学东西要举一反三嘛,然后发现一个很有趣的地方,(其实只是自己以前没有注意到而已,汗颜)
我将参数换成了int类型,然后执行程序后,查看日志后我发现了这样一个SQL语句,而且返回了我想要的结果!!!
SELECT * FROM B_USER_ROLE_MAPPING WHERE hid='294\' OR \'1\'=\'1'
首先一点,这验证了上面说的PrepareStatement处理参数的方式(所有类型的参数),即将参数的前后加上单引号,然后对所有的特殊字符进行转义。
然后我发现这条语句就相当于SELECT * FROM B_USER_ROLE_MAPPING WHERE hid='294'

也相当于SELECT * FROM B_USER_ROLE_MAPPING WHERE hid=294

扫描二维码关注公众号,回复: 1520057 查看本文章
这就涉及到MySQL对字符串转换的过程,这个就留给感兴趣的去深究。

2.3疑问

问:PreparedStatement的生命周期跟Statement一样,在一个数据库连接connection范围内有效,所以说如果一次连接中对于同一个PreparedStatement处理多次(参数不同),那么用PreparedStatement是可以提高效率,但大多情景都是多次连接中处理同一个PreparedStatement,那么就算使用了PreparedStatement也不能提高效率,那么如何正确的使用PreparedStatement呢?
带着这个疑问我又查看文档(其实是问了度娘)发现:
答:J2EE服务器的连接池管理器已经实现了缓存的使用。J2EE服务器保持着连接池中每一个连接准备过的prepared statement列表。当我们在一个连接上调用preparedStatement时,应用服务器会检查这个statement是否曾经准备过。如果是,这个PreparedStatement会被返回给应用程序。如果否,调用会被转给JDBC驱动程序,然后将新生成的statement对象存入连接缓存。
所以由这个发现又引发了我对数据库连接池原理一探究竟的想法,所以下一次写博客就来讲讲我对数据库连接池的原理的学习和了解。

本人刚入职场,小白一名,文章如果有什么错误欢迎指正,谢谢!!!
如果文章对你有一点点作用的话,欢迎留言,我们可以交流一下,互相进步!!!
如果有幸被人转载的话,请注明出处,万分感谢!!!!

猜你喜欢

转载自blog.csdn.net/aimomo007/article/details/77609325