MyBatis and SQL injection that MyBatis may cause

MyBatis is an excellent persistence layer framework that supports customized SQL, stored procedures, and advanced mapping. MyBatis avoids almost all of the JDBC code and manual setting of parameters and fetching result sets. MyBatis can configure and map native information using simple XML or annotations to map interfaces and Java’s POJOs(Plain Old Java Objects) to records in the database.

MyBatis, as I understand it, is to associate database fields with Java objects. Developers do not need to pay attention to database operations, but can directly operate Java objects to complete the operation of adding, deleting, modifying and checking the database. However, in the process of configuring MyBatis, it is unavoidable to write SQL statements in XML configuration files. In some cases, if the SQL statements are not written properly, SQL injection may occur. This article will start from the configuration of MyBatis framework to talk about the causes of SQL injection and prevention methods.

Before configuring MyBatis, prepare the Jar package of MyBatis or use Maven to automatically download the JAR package.

The jar package can be downloaded from www.mybatis.cn/82.html

This demo uses MAVN to open IDEA to create a Maven project

MyBatis dependency is configured in Pop.xml. Maven will automatically download the dependency to the local project.

< the groupId > org. Mybatis < / groupId > < artifactId > mybatis < / artifactId > < version > 3.5.3 < / version >Copy the code

Before running MyBatis first understand the execution process of MyBatis, help to write MyBatis code.

MyBatis constructs the SqlSessionFactory (session factory) by reading configuration file information (global configuration file and mapping file).

MyBatis configuration file, including MyBatis global configuration file and MyBatis mapping file, which configures data source, transaction and other information; The mapping file configures information related to SQL execution.

MyBatis can create a SqlSession using SqlSessionFactory. MyBatis can create a SqlSession using SqlSessionFactory.

Step 3: MyBatis operates the database. SqlSession itself does not operate directly on the database; it does so through the underlying Executor Executor interface. The Executor interface has two implementation classes, a normal Executor and a cache Executor (the default). The Executor Executor processes SQL information encapsulated in an underlying object, MappedStatement. This object includes SQL statements, input parameter mapping information, and output result set mapping information. The mapping types of input parameters and output results include Java simple types, HashMap collection objects, and POJO object types.

It can be seen that the operation of MyBatis needs to read the configuration file. The configuration file is in XML format, and each field has different meanings. The specific configuration file is as follows:

<! <environments default=" MySQL "> <environment id=" MySQL "> <! <transactionManager type="JDBC"/> <! --dataSource indicates the connection source configuration, <dataSource type="POOLED"> <property name="driver" value=" com.mysql.jdbc.driver "/> The < property name = "url" value = "JDBC: mysql: / / 127.0.0.1:3306 / person" / > < property name = "username" value = "root" / > < property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <! Mybatis: where to find the mapping file of persistent class, SRC file name, if in a package, specify path. com/mybatistest/config/Mapper.xml--> <mapper resource="Mapper.xml"/> </mappers>Copy the code

As you can see from the above configuration file, MyBatis does not need to connect to the database directly. It also needs to use the JDBC driver. The fields specify the mapping file of Mybatis to the Java code, which will be mentioned later.

The mapper. XML file is then configured by creating a Person entity class with attributes consistent with the database fields:

package cn.dk;

public class Person

{

private int id;

private String name;

private int age;

private String address;


public Person(int id, String name, int age, String address) {

    this.id = id;

    this.name = name;

    this.age = age;

    this.address = address;

}


public int getId() {

    return id;

}


public String getName() {

    return name;

}


public int getAge() {

    return age;

}


public String getAddress() {

    return address;

}


public void setId(int id) {

    this.id = id;

}


public void setName(String name) {

    this.name = name;

}


public void setAge(int age) {

    this.age = age;

}


public void setAddress(String address) {

    this.address = address;

}

@Override

public String toString() {

    return "Person{" +

            "id=" + id +

            ", name='" + name + '\'' +

            ", age=" + age +

            ", address='" + address + '\'' +

            '}';

}
Copy the code

}

With the entity class mentioned above, the mapper.xml file is configured to serve this entity class, as follows:

<select id="GetUserByID" parameterType="int" resultType="cn.dk.Person">

    select * from user where id = #{id}

</select>
Copy the code

<mapper namespace=”cn.dk.Person” Specifies a unique namespace for this mapper. This namespace is customically set to the full path of the mapping file to preserve the uniqueness of the name.

id:用于标识这个select语句,这样就可以通过namespace加id找到唯一的sql语句。parameterType:指定查询是传入的参数类型。 resultType:即返回结果集的类型,这理指定为Person对象类型。 select * from user where id = #{id} 这个语句中的#{}表示要动态传入的值,sql注入往往发生在这里,具体情况后面会讲到。 当parameterType参数为简单类型时(8个基本类型+String),则sql语句中可以使用任何占位符,当parameterType为对象类型时,则sql语句中的占位符必须写属性名。 MyBatis基础查询方式 到这里配置文件和映射文件都有了,就可以使用java代码来操作数据库了,写一个test类来进行验证。 package cn.dk; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.io.Reader; public class Test { public static void main(String[] args) throws IOException { //加载配置文件(为了连接数据库) InputStream is = Resources.getResourceAsStream(“config.xml”); //初始化mybatis,创建SqlSessionFactory类实例 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); //创建Session实例 SqlSession session = sqlSessionFactory.openSession(); //通过Mapper.xml文件找到sql语句 String Statement = “cn.dk.Person.Mapper.GetUserByID”; 执行查询 Person person = session.selectOne(Statement,1); System.out.println(person); } } 上述代码执行的结果为: 可以看到已经将数据查出,执行的为sql语句,却和java对象关联到了一起,这就是MyBatis的方便之处。 MyBatis接口查询方式 上述方式为MyBatis的基础CRUD方法,除此外MyBatis还有另外一种方式称为MyBatis接口开发模式,MyBatis接口开发模式基于动态代理。 这种方式在基础查询方式之上加了一个接口类,接口类的实现必须满足以下要求: 方法名和Mapper.xml中的id值相同 方法的输入参数和Mapper.xml中的parameterType一致 方法的返回值和Mapper.xml中resultType一致 package cn.dk; public interface Mapper { /* 1. 方法名和Mapper.xml中的id值相同 2. 方法的输入参数和Mapper.xml中的parameterType一致 3. 方法的返回值和Mapper.xml中resultType一致 */ public abstract Person GetUserByID(int id); } 除了以上约定,要实现接口中的方法和Mapper.xml中的Sql语句一一对应还需要将Mapper.xml中的namespace值改为接口的全类名。 这样就可以使用session对象操作数据库了,和基本查询方法不同的是此种方法需要使用getMapper()方法反射获取Mapper对象。 完整代码: package cn.dk; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class Test { public static void main(String[] args) throws IOException { //加载配置文件(为了连接数据库) InputStream is = Resources.getResourceAsStream(“config.xml”); //初始化mybatis,创建SqlSessionFactory类实例 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); //创建Session实例 SqlSession session = sqlSessionFactory.openSession(); Mapper mp = session.getMapper(Mapper.class); Person person = mp.GetUserByID(1); System.out.println(person); session.close(); } } 执行结果 上面演示的都是查询单个结果,那么如何进行多个结果的查询呢,也就是sql语句需要传入多个参数,实现传多个参数的方法有很多种,我这里就使用了最简单的一中方式:匿名传参。其它的传参方式请参考:https://blog.csdn.net/bdqx_007/article/details/94836637 MyBatis规定匿名传参时必须传入param1,param2这种方式的,例如: <select id=”GetUserByID” parameterType=”int” resultType=”cn.dk.Person”> select * from user where id = #{param1} or id = #{param2} </select> 然后Mapper接口中的方法使用List接收多个返回结果就可以了。 package cn.dk; import java.util.List; public interface Mapper { public abstract List<Person> GetUserByID(int param1, int param2); } 执行结果: MyBatis两种取值符号 在上面的演示实例中,只使用到了MyBatis的一种取值方式也就是#{}这种方式的。 除了这种方式还有另外一种方式${value}。 当parameterType为简单类型时,当取值符号为时,{}中的值必须为value。当parameterType为对象时,{}中的值为对象属性名。 这两种取值符号的差别是:#{}会自动传入值加上单引号,而{}不会。 请注意这个差别,sql注入的产生的原因之一就是因为取值符号使用${}时,用户传入的值含单引号引起的。 下面演示下${value}这种传值方式。 修改Mapper.xml文件中的sql查询语句: <select id=”GetUserByID” parameterType=”String” resultType=”cn.dk.Person”> select * from user where name = ${value} </select> 然后测试类: 这里有个坑点,先运行下看下结果: 前面说了${}这种传值方式不会给传入的值添加引号的,所以我们传入的String类型带到数据库中查询时也不会加引号,从而导致sql查询报错。解决办法是手动在xml文件中添加引号,如下: 但是{}这种方式在动态排序时更加好用,比如当需要根据数据库字段id进行降序排列查询结果,#{}由于会给传入的值自动加上引号,导致查询语句变为了select * from user order by ‘id’ desc,此时会根据一个字符常量进行排序,显然不能得到我们想要的结果,此时就必须使用{}这种方式了,因此在涉及到排序相关的业务时很容易导致sql输入的产生。 举一个例子,当我们需要执行的sql语句为:select * from user order by id desc时,MyBatis中的传值符号使用#{},看能否得到我们想要的结果: 可以看到排序结果并不是降序,当把传值方式改为${}时即可得到正确的结果 : MyBatis可能导致的注入 上面我们讲了MyBatis的两种传值方式,现在来总结一下两者的差别: #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by “111”, 如果传入的值是id,则解析成的sql为order by “id”。 将传入的数据直接显示生成在sql中。如:order by user_id,如果传入的值是111,那么解析成sql时的值为order by user_id, 如果传入的值是id,则解析成的sql为order by id。 #方式能够很大程度防止sql注入,因为#{}实现 SQL 语句的参数化,避免直接引入恶意的 SQL 语句并执行。 方式无法防止Sql注入。 方式一般用于传入数据库对象,例如传入表名。 一般能用#的就别用。 所以大多数sql注入产生的原因都是因为开发者在xml文件中使用了${}传值方式。 MYBatis最有可能产生注入的三种情况: SQL语句中的一些部分,例如order by字段、表名等,是无法使用预编译语句的。这种场景极易产生SQL注入。推荐开发在Java层面做映射,设置一个字段/表名数组,仅允许用户传入索引值。这样保证传入的字段或者表名都在白名单里面。 like参数注入。使用如下SQL语句可防止SQL注入 like concat(‘%’,#{title}, ‘%’) in之后参数的SQL注入。使用如下SQL语句可防止SQL注入 id in <foreach collection=”ids” item=”item” open=”(“separator=”,” close=”)”> #{item} sql注入演示: 接着上一个章节提到的Order by 查询来演示下sql注入,我们提到${}这种取值方式是不会自动添加引号的,当我们传入的payload变为id and extractvalue(1,concat(0x7C,(select user()),0x7C))时,sql注入就产生了。 最后以freeCMS为例看下MyBatis的sql注入。MyBatis有很多工具可以根据对象实体自动生成Mapper接口和映射文件,其中最常用的应该是Mybatis-Generator,关于Mybatis-Generator的使用方法请参考https://www.cnblogs.com/throwable/p/12046848.html。遗憾的是Mybatis-Generator生成的Mapper映射文件order by查询也是使用的${}这种方式,如果开发者没有在业务层对用户输入进行过滤就会导致sql注入的产生。freeCMS正是由于这个原因导致的sql注入,freeCMS分为免费版和商业版,免费版的最新版本为1.5,下载地址:http://www.freeteam.cn/。截至目前位置,最新版本依旧存在sql注入漏洞,据说上夜班也存在这个漏洞。 freeCMS的安装过程就不说了,官网有操作手册。 freeCMS有个会员积分查询功能,漏洞点就存在这里,首先看下此功能对应的Mapper映射文件: order by 参数使用了${}进行传入,继续查看其业务层实现逻辑: 并未对order传参进行任何过滤,紧接着就是Action的实现了: 如果order为空则默认值为credittime desc,否则就按照传入的值进行排序。order传参可控且并未进行任何过滤,那么就导致了sql注入的产生: 如何防范MyBatis sql注入的产生呢?这里引用madneal师傅的总结: 能不使用拼接就不要使用拼接,这应该也是避免 SQL 注入最基本的原则 在使用 {} 传入变量的时候,一定要注意变量的引入和过滤,避免直接通过 {} 传入外部变量 不要自己造轮子,尤其是在安全方面,其实在这个问题上,框架已经提供了标准的方法。如果按照规范开发的话,也不会导致 SQL 注入问题 可以注意 MyBatis 中 targetRuntime 的配置,如果不需要复杂的条件查询的话,建议直接使用 MyBatis3Simple。这样可以更好地直接杜绝风险,因为一旦有风险点,就有发生问题的可能。 参考文章 审计mybatis的sql注入 MyBatis 和 SQL 注入的恩恩怨怨 Mybatis框架下SQL注入审计分析