JDBC
什么是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列表可以看到数据库中所有的名字都被读取了出来.
使用占位符(PreparedStatement)
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的开启与关闭情况
在对
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,like的危害
order by
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
在使用like时,通过平常的sql语句进行构造
select * from students where name like '%?%'
会报错,所以有时我们看到的代码中会出现拼接形态的like语句,此时就很有可能出现sql注入漏洞
正确的like预编译构造方法如下图所示,需要在
setString()
方法中将%构造出来
Mybatis
Mybatis解析执行过程
引用一下先知社区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语句进行校验,在其中将
#{}
替换成了
?
like
在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}, '%')
order by
至于
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
版权归原作者 鸣蜩十四 所有, 如有侵权,请联系我们删除。