什么是JDBC?JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。
使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌───────────────┐ │
│ Java App │
│ └───────────────┘ │
│
│ ▼ │
┌───────────────┐
│ │JDBC Interface │<─┼─── JDK
└───────────────┘
│ │ │
▼
│ ┌───────────────┐ │
│ JDBC Driver │<───── Vendor
│ └───────────────┘ │
│
└ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┘
▼
┌───────────────┐
│ Database │
└───────────────┘
通过SQLInjection.java代码对于JDBC中对于数据的调用使用预编译和不使用预编译两种情况进行分析
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
Statement stmt = conn.createStatement();
String sql = "select name from students where id =" + value;
ResultSet rs = stmt.executeQuery(sql);
不使用占位符拼接的关键代码如上,先通过Connection提供的createStatement()方法创建一个stmt对象,用于执行一个查询.然后执行stmt对象提供的executeQuery(sql)传入我们构造的SQL语句并获得返回的结果集,使用ResultSet来引用结果集.
而在执行的关键代码中,先是把sql语句传入Statementlmpl.class
当中的ResultSet executeQuery()
方法中,在locallyScopedConn.execSQL()
中执行SQL并将执行结果放入到this.result
中
通过追踪execSQL()
方法可以追溯到Connectionlmpl.class
文件,我们可以看到在将sql语句接收到方法中后,将语句交由MysqlIO来执行.
查看sqlQueryDirect()
方法,通过拼装发送包信息,最后通过Buffer resultPacket = this.sendCommand(3, (String)null, queryPacket, false, (String)null, 0);
中的sendCommand()
方法将其发送出去
纵观在Statementlmpl.class
当中的ResultSet executeQuery()
方法中只是将我们的sql语句进行一步步的传递,大部分只进行了功能上的校验,在最后发送到数据库进行执行,通过names列表可以看到数据库中所有的名字都被读取了出来.
String sql = "select name from students where id =?";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
preparedStatement.setString(1, value);
ResultSet resultSet = preparedStatement.executeQuery();
使用PreparedStatement
预编译方法,对于要传递的id的值先使用?进行占位,并且把数据连同sql本身传给数据库,以此保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同.
我们传递数据1 or 1=1
进行测试,以此来学习在PrepareStatement
对于我们传入的数据的处理过程.
在对prepareStatement()
方法进行调试的时候,我们需要了解一个关于预编译的知识点.
预编译功能跟MySQL版本及 MySQL Connector/J(JDBC驱动)版本都有关,首先MySQL服务端是在4.1版本之后才开始支持预编译的,之后的版本都默认支持预编译,并且预编译还与 MySQL Connector/J(JDBC驱动)的版本有关, Connector/J 5.0.5之前的版本默认支持预编译, Connector/J 5.0.5之后的版本默认不支持预编译, 所以我们用的Connector/J 5.0.5驱动以后版本的话默认都是没有打开预编译的 (如果需要打开预编译,需要配置 useServerPrepStmts 参数)
因为我的测试环境为5.1.47,所以目前版本的预编译默认是关闭的
所以我们运行代码,在初始状态下的mysql查询日志是这样的
而在数据库链接中加入useServerPrepStmts=true
后mysql的查询日志为
我们可以看到查询比之前多了一条Prepare数据,表示着预编译开启成功
我们回到代码调试中,当代码执行到PreparedStatement preparedStatement = conn.prepareStatement(sql);
时,我们通过断点调试可以进入到ConnectionImpl,java
的prepareStatement()
方法当中.
当我们没有设置useServerPrepStmts=true
时,在prepareStatement()
方法当中useServerPreparedStmts
属性为false,直接跳过当前代码块进入到最后的else代码块
最后并不会向数据库提交SQL预编译请求
而我们设置useServerPrepStmts=true
后,再次调试代码,会发现useServerPreparedStmts
属性为true,最后向数据库提交SQL预编译请求
我们继续调试代码到ResultSet resultSet = preparedStatement.executeQuery()
,进入setString()
方法
在没有开启预编译的情况下,会进入PreparedStatement.class
,在其中的isEscapeNeededForString()
方法中对于用户输入的数据中的非法字符进行转义,最后交由数据库端进行运行.
而开启了预编译的情况下,会进入ServerPreparedStatement.class
,最后数据交由mysql端进行转义处理
ORDER BY关键字用于按升序或降序对结果集进行排序。
order by
后一般接字段名,而字段名是不能带引号的,比如order by id
,如果使用预编译,id在预编译的过程中会被setString()方法自动加引号,而如果带上引号之后就成了order by 'id'
,现在id就是一个字符串而不是一个字段名了,会产生语法错误.
可以看到拼接后的sql语句中order by
的参数就是字符串,我们在数据库中运行查看
可以看出经过引号包裹的语句没有起作用
所以在开发过程中,不能参数化的位置,不管怎么拼接,最终都是和使用"+"号拼接字符串的功效一样:拼成了sql语句但没有防sql注入的效果.
我们可以通过构造if语句来对order by
以后的语句进行构造进行SQL注入
所以需要对order by参数进行特殊的过滤
在使用like时,通过平常的sql语句进行构造select * from students where name like '%?%'
会报错,所以有时我们看到的代码中会出现拼接形态的like语句,此时就很有可能出现sql注入漏洞
正确的like预编译构造方法如下图所示,需要在setString()
方法中将%构造出来
引用一下先知社区R17a大佬的过程图:
以查询SQL分析,主要步骤如下:
1.SqlSession创建过程:SqlSessionFactoryBuilder().build(inputStream)
创建一个SqlSession
,创建的时候会进行配置文件解析生成Configuration属性实例,解析时会将mapper解析成MapperStatement加到Configuration中,MapperStatement是执行SQL的必要准备,SqlSource是MapperStatement的属性,实例化前会先创建动态和非动态SqlSource即DynamicSqlSource
和RawSqlSource
,DynamicSqlSource
对应解析$
以及动态标签如foreach,RawSqlSource创建时解析#
并将#{}
换成占位符?
;
2.执行准备过程:DefaultSqlSession.selectOne()
执行sql(如果是从接口getMapper
方式执行,首先会从MapperProxy
动态代理获取DefaultSqlSession
执行方法selectxxx|update|delete|insert)
,首先从Configuration获取MapperStatement
,执行executor.query()
。executor执行的第一步会先通过MapperStatement.getBoundSql()
获取SQL,此时如果MapperStatement.SqlSource
是动态即DynamicSqlSource
,会先解析其中的动态标签比如${}
会换成具体传入的参数值进行拼接,获取到SQL之后调用executor.doQuery()
,如果存在预编译首先会调用JDBC处理预编译的SQL,最终通过PreparedStatementHandler
调用JDBC执行SQL;
3.JDBC执行SQL并返回结果集
在通过mybatis数据操作的过程中,在XMLScriptBuilder.parseScriptNode()
处会因为${}
和#{}
使用的不同执行不同的方法
在进入parseScriptNode()
后,先通过parseDynamicTags()
方法中的TextSqlNode.isDynamic()
判断是否存在${}
标志来区分动态和非动态SqlSource
TextSqlNode.isDynamic()
首先会通过DynamicCheckerTokenParser()
中的GenericTokenParser()
创建一个${}
标识符解析
继续下一步调用GenGenericTokenParser.parse
对我们的SQL语句进行校验
在parse()
中可以看到如果在我们sql语句中发现了${
那么继续执行,如果没有就直接返回,而在继续执行的最后调用了builder.append(this.handler.handleToken(expression.toString()))
,在handler.handleToken
中将isDynamic
更改为了true
当isDynamic
为true,会实例化一个DynamicSqlSource
对象,返回sqlSource
从上面关于${}的分析可以知道,如果我们的sql语句构造为#{},那么将在XMLScriptBuilder.parseScriptNode
方法中使用RawSqlSource
来构造sqlSource
在过程中同样经过GenGenericTokenParser.parse
对我们的SQL语句进行校验,在其中将#{}
替换成了?
在mybatis中错误的like使用语句为select * from user where name like "%${id}%"
通过构造id = 1%" or 1=1 #
使最后在数据库执行select * from user where name like "%1%" or 1=1 # %"
正确的构造方法应该为SELECT * FROM user where name like concat('%',#{name}, '%')
至于oeder by
的话,和JDBC中的分析相似,如果order by后面跟的变量的话,应该进行校验和过滤
https://www.liaoxuefeng.com/wiki/1252599548343744/1321748435828770
https://juejin.cn/post/6844903490058190862
https://xz.aliyun.com/t/10593
https://xz.aliyun.com/t/10686