环境
Java: jdk_8u65
Mysql: 9.1.0
Mysql可视化:DBeaver 24.3.2
JDBC
JDBC(Java Database Connectivity) 是一个 API(应用程序编程接口),用于 Java 应用程序与数据库之间的连接。它是直接用于数据库操作的接口,而不是介于应用程序和数据库之间的中间层。
1、Statement SQL语句拼接造成SQL注入
1 | String sql = "select * from person where username = '"+username+"' and password = '"+password+"'"; |
2、PreparedStatement 预编译使用不当
预编译先编译 sql语句,无论后续用户输入如何,都作为字符串数据处理,而 sql注入只针对编译的过程进行破坏,所以预编译可以防御 sql注入。
并且还减少了 sql语句的编译次数,提高了性能。
正确的预编译语句如下所示:
1 | String sql = "select * from person where username = ? and password = ?"; |
但是预编译使用不当也会出现 sql注入。
(1) 未使用占位符,而是直接拼接
虽然使用了预编译,但是没有使用 ? 进行占位,所以本质上还是直接拼接导致的 sql注入。
1 | //创建sql语句,直接拼接 |
(2) in语句查询
使用 in语句的场景有很多,比如收藏场景,勾选多篇文章,同时进行收藏,请求包可能如下所示:
1 | messageId=223,556,889 |
当从数据库进行查询时,因为不清楚用户选中的文章的数量,开发可能会直接使用拼接的方式构造查询语句,导致sql注入。
1 | String sql = "select * from person where id in ("+id+")"; |
修复:
可以先对用户传入的数据进行处理,确定对象的个数,根据个数增加占位符,再使用预编译。
比如 ‘取消收藏’的场景:
1 | public int gradeDelete(Connection con, String delIds) throws Exception{ |
(3) Like语句模糊查询
有些业务会使用模糊查询,比如搜索框等场景
1 | String sql = "select username from person where username like '%" + username + "%'"; |
修复:
和 in语句类似,先对用户输入的数据进行判断,再添加占位符进行预编译:
1 | boolean jud = true; |
但是 like的预编译并非百分百可靠,可以通过特殊字符 % 和 _ 来实现注入。
演示代码:
1 | String sql = "select * from person where username like ?"; |
使用 % 和 _ 会导致返回所有结果,绕过预编译防御,所以要对 % 和 _ 进行过滤处理。
(5) Order by注入
ORDER BY
是 SQL 中用于对查询结果进行排序的子句。它按照指定的列或表达式对数据进行升序(ASC
)或降序(DESC
)排列。默认情况下,ORDER BY
按升序排序。
正确的 order by查询使用如下所示:
1 | select * from person order by id desc |
其中 id为列名,意思是按照 id的大小进行生序排序。
但是预编译有一个特性,会强制给占位符所传入的数据加上 ‘’,’id’会被视为字符串而非列名,这样就不会按照预期进行排序。
所以在使用 JDBC连接的前提下想要使用 order by,就不能再使用预编译了,只能通过做好过滤,加WAF等方式来防御 sql注入。
Mybatis
MyBatis 是一款广泛使用的 Java 持久层框架,它通过将数据库操作与 Java 对象进行映射,简化了数据库操作的过程。MyBatis 允许开发者直接编写原生 SQL 查询语句,并通过 XML 或注解将 SQL 与 Java 方法进行映射,使得数据库操作更加灵活和高效。MyBatis 是一个 半自动化 的框架,开发者需要手动编写 SQL 语句,但它提供了映射功能,使得 SQL 查询结果可以直接映射为 Java 对象。
主要组件和原理
SqlSessionFactory:
SqlSessionFactory
是 MyBatis 的核心工厂类,负责创建SqlSession
对象。它会根据全局配置文件(mybatis-config.xml
)读取配置并初始化相关信息。
SqlSession:
SqlSession
是 MyBatis 的核心接口,用于执行 SQL 查询、插入、更新、删除等操作。通过SqlSession
,你可以获取到映射器接口的代理对象,并执行相应的 SQL 操作。
Mapper 接口
Mapper 接口定义了数据库操作的方法,通常这些方法与 SQL 映射文件中的 SQL 标签一一对应。Mapper 接口通常由开发者编写,方法名与 XML 映射文件中的
id
匹配。
映射器 XML 文件
- 这个文件包含了 SQL 语句的定义,MyBatis 根据方法名称查找相应的 SQL 语句,并将查询结果映射成 Java 对象。
通过 id与 UserMapper接口中的方法名进行匹配。resultType指定数据库查询的结果会被映射到哪一个Java类,这里查询结果被映射到了 com.best.hello.entity.User
类中。
Mybatis的两种传参方式
${}: 将传入的参数直接拼接到 sql查询语句中,不做任何的处理。
#{}: 和预编译占位符异曲同工,先进性预编译后传参。将传入的数据都当作字符串,对传入的数据都加上双引号 “”。
(1) ${}直接拼接
某些开发使用 Mybatis框架时,会使用 ${}传参导致 sql注入:
1 | <select id="getInformation" resultType="com.best.hello.entity.User"> |
修复应使用 #{}:
1 | <select id="getInformation" resultType="com.best.hello.entity.User"> |
(2) in, Like查询
like,in查询语句同理,使用 #{}即可。
Like:
1 | mysql: |
in:
1 | mysql: |
(3) Order by注入
与 Jdbc同理,若使用 #{}会给传参加上双引号,导致列名被当作字符串解析,所以不得不使用 ${},由此产生 sql注入。
Mapper接口:
1 | public interface UserMapper { |
映射器xml文件:
1 | <select id="orderBy" resultType="com.best.hello.entity.User"> |
一个很好的修复方法是 ‘排序映射’,以我的理解,是一种类似于白名单的机制,严格限制用户输入:
1 | <select id="orderBySafe" resultType="com.best.hello.entity.User"> |
用户的输入只能是 ‘id’ 和 ‘user’,若为其他直接使用默认排序,不再相信用户输入。
Mybatis-plus
Mybatis-Plus(简称MP)是基于MyBatis的增强工具,它简化了MyBatis的操作,提供了一些常用的功能,目的是让开发者在使用MyBatis时,能够更加高效、便捷地进行数据库操作。Mybatis-Plus不需要重写SQL语句,就能自动完成增、删、改、查等基本操作,减少了大量的代码重复性。
与 Mybatis不同,Mybatis-plus省去了 Mybatis所必需的映射器xml文件。
为了加深印象,我自己搭了一个使用 SpringBoot + Mybatis-plus的环境,添加依赖和数据库配置文件不多赘述。
Application.propertirs:
1 | spring.application.name=SQL_Mybatis |
创建实体类 - 可以类比为 Mybatis中的 resultType:
1 | package org.kgty.sql_mybatis; |
mapper层,继承 BaseMapper,BaseMapper中定义好了很多现成的 sql查询语句,供开发可以直接使用:
1 | package org.kgty.sql_mybatis.mapper; |
service层:eq -> equal的意思,即 where username = username(用户输入)
1 | package org.kgty.sql_mybatis.service; |
Controller层:
1 | package org.kgty.sql_mybatis.controller; |
启动项目,即可连接数据库,进行查询操作:
BaseMapper中提供的查询方法都使用了预编译来避免 sql注入。
(1) apply()直接拼接导致 sql注入
1 | public List<person> getPersons(String username) { |
打印日志如图所示:
如上代码使用 apply()方法会将 username直接拼接到 sql查询语句中,不进行任何预处理,导致 sql注入。
修复方法很简单,apply()也有预编译:
1 | public List<person> getPersons(String username) { |
使用 {0} 充当占位符,这样写可以达到预编译的效果,如下图打印日志所示:
(2) last()使用不当
在 MyBatis-Plus 中,last()
方法用于在 SQL 语句的末尾拼接自定义的 SQL 片段。这对于在查询中添加一些特定的 SQL 语句非常有用,比如排序、分页或其他一些自定义条件。
1 | public List<person> getPersons(String username) { |
直接将 username拼接到 sql语句中,再去执行,造成 sql注入。
修复建议:
对用户的输入数据进行规范处理,采用过滤等操作,并且应尽量避免使用 last()。
(3) exists()和 notExists()使用不当:
EXISTS
和 NOT EXISTS
常常用于查询中,需要根据某个条件是否存在来过滤数据。一般情况下,这类子查询会嵌套在 WHERE
子句中,判断某个数据是否满足特定条件。
业务场景:
比如有两个表 user
和 order
,order
表中的每个订单都与 user
表中的某个用户相关联。如果你想查询那些有订单的用户,可以使用 EXISTS
子查询:
1 | public List<User> getUsersWithOrders() { |
最终的查询语句为:
1 | SELECT * FROM user WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = user.id); |
这个查询会返回那些有至少一个订单的用户。
notExists()同理。
但是如果直接拼接用户可控的输入就会造成 sql注入:
1 | public List<person> getPersons(String username) { |
修复可以继续采用 {index}的方式:
1 | queryWrapper.exists("select * from person where username = {0}", username); |
(4) having()使用不当
having()
主要用于对分组后的数据进行条件过滤。在一些需要对聚合结果进行筛选的场景下,having()
非常有用。例如,计算销售总额、订单数量等指标,并对这些指标进行过滤。
比如:查询订单数量大于 10 的用户:
假设我们有一个 orders
表,包含 user_id
和 order_id
等字段。如果我们想查询订单数量大于 10 的用户,可以使用 having()
方法:
1 | public List<Order> getUsersWithMoreThan10Orders() { |
这个查询会生成如下 SQL:
1 | SELECT user_id, COUNT(*) as order_count |
但是如果直接拼接用户输入就会造成 sql注入:
1 | public List<person> getPersons(String username) { |
类似的写法还有:只不过上面那个是字符型注入,下面这个是数字型注入罢了。
1 | wrapper.select().groupBy("author").having("id > " +id); |
依然是 {idnex}即可避免注入:
1 | queryWrapper.groupBy("id").having("username = {0}", username); |
(5) order by注入
orderBy(true, true, id) - 升序
orderBy(true, false, id) - 降序
orderByAsc() - 升序
orderByDesc() - 降序
order by注入与前面几种不同的是,order by不可以进行预编译,因为会强加上单引号,所以不能使用 {index}来进行修复了。
如果 order by 之后的数据用户可控,就存在 sql注入的风险。
1 | queryWrapper.select().orderByDesc(username); |
时间盲注:
(6) inSql()和 notInSql()使用不当:
insql()
方法用于在 WHERE
子句中添加一个 IN
子查询条件。IN
子句会检查字段的值是否存在于由子查询返回的结果集中。
假设我们有一个 orders
表,想查询所有 user_id
在某些 order_id
查询结果中的用户。可以使用 inSql()
方法来构造这个查询:
1 | QueryWrapper<User> queryWrapper = new QueryWrapper<>(); |
生成的 SQL查询语句如下:
1 | SELECT * FROM user WHERE id IN (SELECT user_id FROM orders WHERE order_date > '2023-01-01'); |
总结了一下,Mybatis-plus出现的 sql注入本质上仍然是未进行预编译的直接拼接导致的。还有一些文章将 wrapper自定义 sql语句导致注入 和 使用xml文件进行映射产生的注入单独拿出来说,我认为最终还是会归咎到上面几种注入中,所以不过多赘述了。