只要你学JDBC,基本上所有的人都会和你说,Statement不能防止SQL注入, PreparedStatement能够防止SQL注入.

基本上参加工作了一段时间之后还是这么认为的, 没错, 这句是没有问题的, 但到底如何进行SQL注入?怎么直观的去了解SQL注入?这还是需要花一定的时间去实验的.

 

前提:以下的测试都是在一种理想环境下

 

首先准备好数据库环境, 以下是数据库的schema:


create database java_mysql;  
  
use java_mysql;  
  
drop table if exists pstest;  
  
create table pstest(  
    id int(10) not null primary key auto_increment,  
    name varchar(32),  
    age int(3)  
);  
  
insert into pstest (name, age) values ('Tom', 23);  
insert into pstest (name, age) values ('Tom1', 23);  
insert into pstest (name, age) values ('Tom2', 23);  
insert into pstest (name, age) values ('Tom3', 23);  
insert into pstest (name, age) values ('Tom4', 23);  
insert into pstest (name, age) values ('Tom5', 23);


 


 以上就是建立了pstest表, 并插入了一些测试数据. 





1. 测试Statement



public class StatementTest {  
    public static void main(String[] args) throws SQLException {  
        Connection con = null;  
        Statement stmt = null;  
        // name很强大, 传入了这么多东西  
        String name = "Tom';delete from pstest;select * from pstest where name='Tom";  
        String sql = createSql(name);   // SQL  
        System.out.println(sql);  
          
        try {  
            con = DBConn.getConnection();  
            stmt = con.createStatement();  
              
            stmt.execute(sql);  
        } catch(Exception e) {  
            e.printStackTrace();  
        } finally {  
            stmt.close();  
            con.close();  
        }  
    }  
      
    // 根据参数的name参数查询  
    private static String createSql(String name) {  
        String sql = "select id, name, age from pstest ";  
          
        // 拼接一下SQL  
        if(name != null && name.length() != 0) {  
            sql += "where name ='" + name + "'";  
        }  
          
        return sql;  
    }  
}



数据库连接的URL为:



"jdbc:mysql://localhost:3306/java_mysql";



其实上面的意图很简单:



Tom';delete from pstest;select * from pstest where name='Tom


就是想先执行一条SQL查询语句,然后把表的数据删除。 



这只是理想环境. 实际上要想传入这么复杂的数据, 真的很难想象




 这里将URL单独拎出来是有作用的, 继续看下面





Run一下StatementTest. 会发现报异常了:


com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'delete from pstest;select * from pstest where name='Tom'' at line 1


  想法很简单, 现实很残酷, 未能如所愿.



看到这里,你也应该想到了Statement的execute(String sql)默认是只能执行一条SQL的.





若想让execute(String sql)能够同时只能几条SQL语句, 怎么办?修改连接的URL:



jdbc:mysql://localhost:3306/java_mysql?allowMultiQueries=true


allowMultiQueries=true这个参数



再来Run一下StatementTest. OK,没有报任何的异常.打印的SQL为:


select id, name, age from pstest where name ='Tom';delete from pstest;select * from pstest where name='Tom';


我们先观察一下:第一条是查询SQL, 第二条是delete, 第三条是查询SQL,其实第一条和第三条是一样的.


为了直观的看上面SQL的执行效果, 我们再次执行下最开始的schema.sql 


这时候数据库有6条数据


mysql> select * from pstest;  
+----+------+------+  
| id | name | age  |  
+----+------+------+  
|  1 | Tom  |   23 |  
|  2 | Tom1 |   23 |  
|  3 | Tom2 |   23 |  
|  4 | Tom3 |   23 |  
|  5 | Tom4 |   23 |  
|  6 | Tom5 |   23 |  
+----+------+------+



执行一下



select id, name, age from pstest where name ='Tom';delete from pstest;select * from pstest where name='Tom';


 这时候可以看到MySQL客户端


mysql> select id, name, age from pstest where name ='Tom';delete from pstest;select * from pstest where name='Tom';  
    +----+------+------+  
    | id | name | age  |  
    +----+------+------+  
    |  1 | Tom  |   23 |  
    +----+------+------+  
    1 row in set (0.00 sec)                 -- 执行第一条查询SQL  
      
    Query OK, 6 rows affected (0.05 sec)    -- 执行第二条delete语句, Oh, No, 数据库全部的6条数据被删除了  
      
    Empty set (0.00 sec)                     -- 执行第三条SQL查询, 没有查询到任何数据


上面的注释已经写好了,就不多说了。





这时候的确已经实现了SQL注入.







2. 测试PreparedStatemet


public class PreparedStatementTest {  
    public static void main(String[] args) throws SQLException {  
        Connection con = null;  
        PreparedStatement ps = null;  
        ResultSet rs = null;  
        String sql = "select id, name, age from pstest where name = ? ";  
          
        try {  
            con = DBConn.getConnection();  
            ps = con.prepareStatement(sql);   
            ps.setString(1, "Tom';delete from pstest;select * from pstest where name='Tom");      
            rs = ps.executeQuery();   
        } catch(Exception e) {  
            e.printStackTrace();  
        } finally {  
            rs.close();  
            ps.close();  
            con.close();  
        }  
    }  
}



直接Run一下,OK,也没出现任何的异常,数据库中的数据也还在



但是我们到底执行了什么样的SQL, 查看MySQL的日志.



这里简单提下MySQL的日志,可以在my.ini下配置


[mysqld]   
log=MySQL_Log    # 在这里加上日志名称



好了,回到正题,看一下刚刚PreparedStatementTest执行的SQL,在MySQL_Log中查看




120719 15:54:25      23 Connect   root@localhost on java_mysql  
           23 Query /* mysql-connector-java-5.1.20-SNAPSHOT ( Revision: ${bzr.revision-id} ) ...  
           23 Query SHOW WARNINGS  
           23 Query /* mysql-connector-java-5.1.20-SNAPSHOT ( Revision: ${bzr.revision-id} ) */SELECT @@session.auto_increment_increment  
           23 Query SHOW COLLATION  
           23 Query SET character_set_results = NULL  
           23 Query SET autocommit=1  
           23 Query select id, name, age from pstest where name = 'Tom\';delete from pstest;select * from pstest where name=\'Tom'  
           23 Quit


 重点是最后一条SQL.



因为数据库中的数据都还在,我们就直接执行这条SQL



select id, name, age from pstest where name = 'Tom\';delete from pstest;select * from pstest where name=\'Tom'


查看MySQL的客户端


mysql> select id, name, age from pstest where name = 'Tom\';delete from pstest;select * from pstest where name=\'Tom';  
   Empty set (0.00 sec)                     -- 就执行了这一条查询的SQL语句


可以看到什么的字符串已经被转义了.



让我们来看一下转义字符:


mysql> select 'Tom\';delete';  
+-------------+  
| Tom';delete |  
+-------------+  
| Tom';delete |  
+-------------+  
1 row in set (0.00 sec)



总结:其实从上面的测试中已经看出了。的确Statement是不安全的, 可以进行SQL注入, 而PreparedStatement可以防止SQL注入。就好比上面我们想在做查询的时候将pstest中的全部数据都删除掉一样. 前面已经说过这是在理想的环境下做的测试. 在真正的环境中,就想这么简单的实现SQL注入, 基本上是不可能的。而且Statemenet,让它执行execute(String sql)的时候同时执行多条SQL, 基本上不可能会去这么做的.





其实,关于Statement的execute(String sql)语句能够同时执行多条SQL语句, 可以看MySQL自带的测试例子:



可查看testsuite.regression包下的ResultSetRegressionTest类:


public class ResultSetRegressionTest extends BaseTestCase {  
    public void testBug33678() throws Exception {  
        if (!versionMeetsMinimum(4, 1)) {  
            return;  
        }  
  
        createTable("testBug33678", "(field1 INT)");  
  
        // allowMultiQueries=true设置  
        Connection multiConn = getConnectionWithProps("allowMultiQueries=true");  
        Statement multiStmt = multiConn.createStatement();  
  
        try {  
            multiStmt.setFetchSize(Integer.MIN_VALUE);  
  
            // 一次性执行多条SQL语句  
            multiStmt  
                    .execute("SELECT 1 UNION SELECT 2; INSERT INTO testBug33678 VALUES (1); UPDATE testBug33678 set field1=2; INSERT INTO testBug33678 VALUES(3); UPDATE testBug33678 set field1=2 WHERE field1=3; UPDATE testBug33678 set field1=2; SELECT 1");  
      
    // 以下代码省略...  
    }  
}