问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

PreparedStatement 是如何防止SQL注入的

创作时间:
作者:
@小白创作中心

PreparedStatement 是如何防止SQL注入的

引用
1
来源
1.
https://www.peterjxl.com/JDBC/1-PreparedStatement/

PreparedStatement是Java中用于执行SQL语句的接口,它通过预编译SQL语句和参数绑定的方式,有效防止了SQL注入攻击。本文将通过具体示例和源码分析,深入探讨PreparedStatement是如何实现这一安全机制的。

数据准备

为了方便演示,我们创建一个user表来模拟登录的情况:

USE learnjdbc;
CREATE TABLE user (
  id BIGINT AUTO_INCREMENT NOT NULL,
  name VARCHAR(50) NOT NULL,
  password VARCHAR(50) NOT NULL,
  PRIMARY KEY(id)
) Engine=INNODB DEFAULT CHARSET=UTF8;
INSERT INTO user (id, name, password) VALUES (1, 'peterjxl', '123456')
INSERT INTO user (id, name, password) VALUES (2, 'peter', '123456')

一个简单的登录逻辑

这里也说明下登录的逻辑:

  1. 用户输入用户名和密码
  2. Java程序收到,并执行SQL判断用户名密码是否存在
  3. 如果一致则认为通过

SQL逻辑如下:

select count(1) from user where name = '用户名' and password = '密码'

如果用户输入的是正确的用户名和密码(例如peterjxl和123456),则执行的SQL是:

select count(1) from user where name = 'peterjxl' and password = '123456'

执行结果是1,说明存在数据,并且用户名和密码都是正确的,认为通过了用户密码校验,登录成功。

相关代码如下:这里假设name和password是用户输入的变量。

String name = "peterjxl";
String password = "123456";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select count(1) from user where name ='" + name + "' and password = '" + password + "'");
while (rs.next()){
    if (0 != rs.getInt(1)){
        System.out.println("登录成功");
    }else{
        System.out.println("登录失败");
    }
}

使用Statement演示SQL注入

如果用户输入的是一个特殊的字符呢?

String name = "peterjxl";
String password = "1' or ''='";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select count(1) from user where name ='" + name + "' and password = '" + password + "'");
while (rs.next()){
    if (0 != rs.getInt(1)){
        System.out.println("登录成功");
    }else{
        System.out.println("登录失败");
    }
}

以上代码拼接SQL后,如下:

select count(1) from user where name = 'peterjxl' and PASSWORD = '1' or ''=''

也就是说,即使用户不知道密码,只要使用SQL注入,就可以登录成功。

更糟糕的情况是,如果用户直接输入了一些删除表和数据库的语句,一旦被执行,后果不堪设想!例如他们可以这样拼接一大串字符:

select count(1) from user where name = 'peterjxl' and PASSWORD = '1' or ''='';
delete from user;
select count(1) from user where ''=''

使用PreparedStatement演示SQL注入

如果我们使用的是PreparedStatement呢?能否SQL注入?我们来试试看:为了方便,第8行我们打印执行的SQL

String name = "peterjxl";
String password = "1' or ''='";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = conn.prepareStatement("select count(1) from user where name = ? and password = ?");
preparedStatement.setObject(1, name); 
preparedStatement.setObject(2, password); 
ResultSet rs = preparedStatement.executeQuery();
System.out.println("sql = " + preparedStatement.toString());
while (rs.next()){
    if (0 != rs.getInt(1)){
        System.out.println("登录成功");
    }else{
        System.out.println("登录失败");
    }
}

运行结果如下:

sql = com.mysql.cj.jdbc.ClientPreparedStatement: select count(1) from user where name = 'peterjxl' and password = '1'' or ''''='''
登录失败

为什么PreparedStatement能防止SQL注入?因为它会把用户输入的单引号做转义,从而防止SQL注入。

PreparedStatement的源码

PreparedStatement只是一个接口,具体的实现类得看数据库厂商实现的源码是怎么样的,这里以MySQL驱动为例,观察其源码。

如果你使用IDE,可以直接在IDE里查看源码,这里以IntelliJ IDEA为例:

然后我们可以打断点调试,注意fillSendPacket方法,将sql转换为字节流的过程中,给占位符中包含的单引号',额外加上一个单引号。

接下来我们继续debug,到底是哪个操作加上了单引号:

找到buildComQuery()方法,有如下代码,其中:

  1. staticSqlStrings[]:存储的是以占位符?划分开的sql语句转换的字节数组,不包含参数,即
  • staticSqlStrings[0]=SELECT * FROM staff_info a WHERE a.staff_age =
  • staticSqlStrings[1]=and a.staff_sex =
  1. bindValues[]:绑定的参数数组

从以上源码可以看出,sql语句和参数共同组装成sendPacket发给远程数据库做执行,而防SQL注入的处理,肯定就在writeAsText()流程中;

我们继续往下,来到com.mysql.cj.protocol.a.StringValueEncoder类中,该类用于处理String参数,于是可以定位到在将参数转换为字节数组的getBytes()方法中对输入的字符串进行了StringUtils.escapeString()处理:

既然已经发现字符串的工具类调用,不出意外我们马上就要找到答案了

可以看到方法中对各类特殊字符都做了转义处理,常规的添加斜杠\如换行符\n;而我们这里涉及的单引号',转义时会再添加一个',这也解释了上述最终执行sql的生成。

注意,不同版本的数据库驱动,代码可能有点不同:

  • MySQL 5的驱动是加上反斜杠\做转义;
  • MySQL 8的驱动是给单引号额外加上一个单引号做转义

但无论如何实现,都能防止SQL注入。

不同版本的MySQL驱动,都是开源的,读者可以去官网下载:MySQL :: MySQL Connector/J 8.0 Developer Guide :: 4.3 Installing from Source

总结

生产环境一定要用PreparedStatement,而不是用Statement!

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号