1. 1. 环境
  2. 2. JDBC
    1. 2.1. 1、Statement SQL语句拼接造成SQL注入
    2. 2.2. 2、PreparedStatement 预编译使用不当
      1. 2.2.1. (1) 未使用占位符,而是直接拼接
      2. 2.2.2. (2) in语句查询
      3. 2.2.3. (3) Like语句模糊查询
      4. 2.2.4. (5) Order by注入
  3. 3. Mybatis
    1. 3.0.1. 主要组件和原理
    2. 3.0.2. Mybatis的两种传参方式
    3. 3.0.3. (1) ${}直接拼接
    4. 3.0.4. (2) in, Like查询
    5. 3.0.5. (3) Order by注入
  • 4. Mybatis-plus
    1. 4.0.1. (1) apply()直接拼接导致 sql注入
    2. 4.0.2. (2) last()使用不当
    3. 4.0.3. (3) exists()和 notExists()使用不当:
    4. 4.0.4. (4) having()使用不当
    5. 4.0.5. (5) order by注入
    6. 4.0.6. (6) inSql()和 notInSql()使用不当:
  • 环境

    Java: jdk_8u65

    Mysql: 9.1.0

    Mysql可视化:DBeaver 24.3.2

    JDBC

    JDBC(Java Database Connectivity) 是一个 API(应用程序编程接口),用于 Java 应用程序与数据库之间的连接。它是直接用于数据库操作的接口,而不是介于应用程序和数据库之间的中间层。

    1、Statement SQL语句拼接造成SQL注入

    1
    2
    String sql = "select * from person where username = '"+username+"' and password = '"+password+"'";
    ResultSet resultSet = statement.executeQuery(sql);

    2、PreparedStatement 预编译使用不当

    预编译先编译 sql语句,无论后续用户输入如何,都作为字符串数据处理,而 sql注入只针对编译的过程进行破坏,所以预编译可以防御 sql注入。

    并且还减少了 sql语句的编译次数,提高了性能。

    正确的预编译语句如下所示:

    1
    2
    3
    4
    5
    6
    7
    String sql = "select * from person where username = ? and password = ?";
    //先编译
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    //后赋值
    preparedStatement.setString(1, username);
    preparedStatement.setString(2, password);
    ResultSet resultSet = preparedStatement.executeQuery();

    但是预编译使用不当也会出现 sql注入。

    (1) 未使用占位符,而是直接拼接

    虽然使用了预编译,但是没有使用 ? 进行占位,所以本质上还是直接拼接导致的 sql注入。

    1
    2
    3
    4
    5
    //创建sql语句,直接拼接
    String sql = "SELECT * FROM users WHERE username = '" + req.getParameter("username") + "' AND password = '" + req.getParameter("password") + "'";
    //预编译sql语句
    PreparedStatement pstt = connection.prepareStatement(sql);
    ResultSet resultSet = pstt.executeQuery(sql);//返回结果集,封装了全部的产部的查询结果
    (2) in语句查询

    使用 in语句的场景有很多,比如收藏场景,勾选多篇文章,同时进行收藏,请求包可能如下所示:

    1
    messageId=223,556,889

    当从数据库进行查询时,因为不清楚用户选中的文章的数量,开发可能会直接使用拼接的方式构造查询语句,导致sql注入。

    1
    2
    3
    String sql = "select * from person where id in ("+id+")";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    ResultSet resultSet = preparedStatement.executeQuery();

    image-20250119121142554

    修复:

    可以先对用户传入的数据进行处理,确定对象的个数,根据个数增加占位符,再使用预编译。

    比如 ‘取消收藏’的场景:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public int gradeDelete(Connection con, String delIds) throws Exception{
    String num = "";
    //将对象分割开来,分割的点以实际而定
    String[] spl = delIds.split(".");

    //根据对象的个数添加同量的占位符?,用来预编译
    for(int i = 0; i< spl.length; i++){
    if(i == 0){
    num += "?";
    } else {
    num += ".?";
    }
    }
    String sql = "delete from users where id in("+num+")";
    prepareStatement pstmt = con.prepareStatement(sql);
    try {
    for(int j = 0; j < spl.length; j++){
    pstmt.setInt(j+1, integer.parseint(spl[j]));
    }
    return pstmt.executeUpdate();
    } catch(Exception e){
    //
    }

    return 0;
    }
    (3) Like语句模糊查询

    有些业务会使用模糊查询,比如搜索框等场景

    1
    2
    3
    4
    5
    String sql = "select username from person where username like '%" + username + "%'";
    //先编译
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    //后赋值
    ResultSet resultSet = preparedStatement.executeQuery();

    image-20250119123430175

    修复:

    和 in语句类似,先对用户输入的数据进行判断,再添加占位符进行预编译:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    boolean jud = true;
    int v = 0;
    String sql = "select * from users ";
    System.out.println("请输入要查询的内容:");
    String con = sc.nextLine();

    //先对用户输入进行处理
    for (int i = 0; i < con.length(); i++){
    if(!Character.isDigit(con.charAt(i))){
    jud = false;
    break;
    }
    }
    if(jud){
    sql += "where password like ?";
    v = 1;
    }else{
    sql += "where username like ? and password like ?";
    v = 2;
    }

    //预编译sql语句
    PreparedStatement pstt = connection.prepareStatement(sql);
    if(v == 1){
    pstt.setString(1, "%"+con+"%");
    }else if (v == 2){
    pstt.setString(1, "%"+con+"%");
    pstt.setString(2, "%"+con+"%");
    }

    但是 like的预编译并非百分百可靠,可以通过特殊字符 % 和 _ 来实现注入。

    演示代码:

    1
    2
    3
    4
    5
    6
    String sql = "select * from person where username like ?";
    //先编译
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    //后赋值
    preparedStatement.setString(1, "%" + username + "%");
    ResultSet resultSet = preparedStatement.executeQuery();

    使用 % 和 _ 会导致返回所有结果,绕过预编译防御,所以要对 % 和 _ 进行过滤处理。

    image-20250119140007769

    (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 匹配。

      image-20250119142210066

    映射器 XML 文件

    • 这个文件包含了 SQL 语句的定义,MyBatis 根据方法名称查找相应的 SQL 语句,并将查询结果映射成 Java 对象。

    image-20250119142259787

    通过 id与 UserMapper接口中的方法名进行匹配。resultType指定数据库查询的结果会被映射到哪一个Java类,这里查询结果被映射到了 com.best.hello.entity.User类中。

    Mybatis的两种传参方式

    ${}: 将传入的参数直接拼接到 sql查询语句中,不做任何的处理。

    #{}: 和预编译占位符异曲同工,先进性预编译后传参。将传入的数据都当作字符串,对传入的数据都加上双引号 “”。

    (1) ${}直接拼接

    某些开发使用 Mybatis框架时,会使用 ${}传参导致 sql注入:

    1
    2
    3
    4
    5
    <select id="getInformation" resultType="com.best.hello.entity.User">
    select *
    from users
    where username=${username} and password=${password}
    </select>

    修复应使用 #{}:

    1
    2
    3
    4
    5
    <select id="getInformation" resultType="com.best.hello.entity.User">
    select *
    from users
    where username=#{username} and password=#{password}
    </select>
    (2) in, Like查询

    like,in查询语句同理,使用 #{}即可。

    Like:

    1
    2
    3
    4
    5
    6
    mysql:
    select * from users where username like concat('%',#{username},'%')
    oracle:
    select * from users where username like '%'||#{username}||'%'
    sqlserver:
    select * from users where username like '%'+#{username}+'%'

    in:

    1
    2
    3
    4
    5
    6
    mysql:
    select * from users where username like concat('%',#{username},'%')
    oracle:
    select * from users where username like '%'||#{username}||'%'
    sqlserver:
    select * from users where username like '%'+#{username}+'%'
    (3) Order by注入

    与 Jdbc同理,若使用 #{}会给传参加上双引号,导致列名被当作字符串解析,所以不得不使用 ${},由此产生 sql注入。

    Mapper接口:

    1
    2
    3
    public interface UserMapper {
    List<User> orderBy(String field, String sort);
    }

    映射器xml文件:

    1
    2
    3
    4
    5
    <select id="orderBy" resultType="com.best.hello.entity.User">
    select *
    from users
    order by ${field} ${sort}
    </select>

    一个很好的修复方法是 ‘排序映射’,以我的理解,是一种类似于白名单的机制,严格限制用户输入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <select id="orderBySafe" resultType="com.best.hello.entity.User">
    select * from users
    <choose>
    <when test="field == 'id'">
    order by id desc
    </when>
    <when test="field == 'user'">
    order by user desc
    </when>
    <otherwise>
    order by pass desc limit 2
    </otherwise>
    </choose>
    </select>

    用户的输入只能是 ‘id’ 和 ‘user’,若为其他直接使用默认排序,不再相信用户输入。

    Mybatis-plus

    Mybatis-Plus(简称MP)是基于MyBatis的增强工具,它简化了MyBatis的操作,提供了一些常用的功能,目的是让开发者在使用MyBatis时,能够更加高效、便捷地进行数据库操作。Mybatis-Plus不需要重写SQL语句,就能自动完成增、删、改、查等基本操作,减少了大量的代码重复性。

    与 Mybatis不同,Mybatis-plus省去了 Mybatis所必需的映射器xml文件。

    为了加深印象,我自己搭了一个使用 SpringBoot + Mybatis-plus的环境,添加依赖和数据库配置文件不多赘述。

    Application.propertirs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    spring.application.name=SQL_Mybatis
    server.port=9988
    # MySQL 数据库连接配置
    spring.datasource.url=jdbc:mysql://localhost:3306/demo_database?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

    # MyBatis-Plus 配置
    #mybatis-plus.mapper-locations=classpath:/mapper/*Mapper.xml
    mybatis-plus.type-aliases-package=org.kgty.sql_mybatis

    # Hibernate/JPA配置(如果使用JPA)
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

    创建实体类 - 可以类比为 Mybatis中的 resultType:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package org.kgty.sql_mybatis;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;

    @Data
    @TableName("person") // 表明实体类对应的表
    public class person {
    private int id; // 主键
    private String username;
    private String password;

    //getter 和 setter方法
    }

    mapper层,继承 BaseMapper,BaseMapper中定义好了很多现成的 sql查询语句,供开发可以直接使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package org.kgty.sql_mybatis.mapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import org.apache.ibatis.annotations.Select;
    import org.kgty.sql_mybatis.person;
    import org.apache.ibatis.annotations.Mapper;

    import java.util.List;

    @Mapper
    public interface personMapper extends BaseMapper<person> {

    }

    service层:eq -> equal的意思,即 where username = username(用户输入)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package org.kgty.sql_mybatis.service;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import org.kgty.sql_mybatis.person;
    import org.kgty.sql_mybatis.mapper.personMapper;
    import java.util.List;

    @Service
    public class personService {
    @Autowired
    private personMapper pm;

    public List<person> getPersons(String username) {
    QueryWrapper<person> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    return pm.selectList(queryWrapper);
    }
    }

    Controller层:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package org.kgty.sql_mybatis.controller;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.kgty.sql_mybatis.person;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    import java.util.List;
    import org.kgty.sql_mybatis.mapper.personMapper;
    import org.kgty.sql_mybatis.service.personService;


    @RestController
    public class personController extends ServiceImpl<personMapper, person> {
    @Autowired
    private personService personService;

    @RequestMapping("/person")
    public List<person> getPersons(@RequestParam String username) {
    return personService.getPersons(username);
    }
    }

    启动项目,即可连接数据库,进行查询操作:

    image-20250120111516266

    BaseMapper中提供的查询方法都使用了预编译来避免 sql注入。

    (1) apply()直接拼接导致 sql注入
    1
    2
    3
    4
    5
    public List<person> getPersons(String username) {
    QueryWrapper<person> queryWrapper = new QueryWrapper<>();
    queryWrapper.apply("username = '"+username+"'");
    return pm.selectList(queryWrapper);
    }

    打印日志如图所示:

    image-20250120122930513

    如上代码使用 apply()方法会将 username直接拼接到 sql查询语句中,不进行任何预处理,导致 sql注入。

    修复方法很简单,apply()也有预编译:

    1
    2
    3
    4
    5
    public List<person> getPersons(String username) {
    QueryWrapper<person> queryWrapper = new QueryWrapper<>();
    queryWrapper.apply("username = {0}", username);
    return pm.selectList(queryWrapper);
    }

    使用 {0} 充当占位符,这样写可以达到预编译的效果,如下图打印日志所示:

    image-20250120123210987

    (2) last()使用不当

    在 MyBatis-Plus 中,last() 方法用于在 SQL 语句的末尾拼接自定义的 SQL 片段。这对于在查询中添加一些特定的 SQL 语句非常有用,比如排序、分页或其他一些自定义条件。

    1
    2
    3
    4
    5
    public List<person> getPersons(String username) {
    QueryWrapper<person> queryWrapper = new QueryWrapper<>();
    queryWrapper.last("order by " + username); //注意 by后面要有空格
    return pm.selectList(queryWrapper);
    }

    直接将 username拼接到 sql语句中,再去执行,造成 sql注入。

    image-20250120124042691

    image-20250120124053178

    修复建议:

    对用户的输入数据进行规范处理,采用过滤等操作,并且应尽量避免使用 last()。

    (3) exists()和 notExists()使用不当:

    EXISTSNOT EXISTS 常常用于查询中,需要根据某个条件是否存在来过滤数据。一般情况下,这类子查询会嵌套在 WHERE 子句中,判断某个数据是否满足特定条件。

    业务场景:

    比如有两个表 userorderorder 表中的每个订单都与 user 表中的某个用户相关联。如果你想查询那些有订单的用户,可以使用 EXISTS 子查询:

    1
    2
    3
    4
    5
    public List<User> getUsersWithOrders() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.exists("SELECT 1 FROM orders o WHERE o.user_id = user.id"); // EXISTS 子查询
    return userMapper.selectList(queryWrapper);
    }

    最终的查询语句为:

    1
    SELECT * FROM user WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = user.id);

    这个查询会返回那些有至少一个订单的用户。

    notExists()同理。

    但是如果直接拼接用户可控的输入就会造成 sql注入:

    1
    2
    3
    4
    5
    public List<person> getPersons(String username) {
    QueryWrapper<person> queryWrapper = new QueryWrapper<>();
    queryWrapper.exists("select * from person where username = '"+username+"'"); // EXISTS 子查询
    return userMapper.selectList(queryWrapper);
    }

    image-20250120182226959

    image-20250120182304057

    修复可以继续采用 {index}的方式:

    1
    queryWrapper.exists("select * from person where username = {0}", username);
    (4) having()使用不当

    having() 主要用于对分组后的数据进行条件过滤。在一些需要对聚合结果进行筛选的场景下,having() 非常有用。例如,计算销售总额、订单数量等指标,并对这些指标进行过滤。

    比如:查询订单数量大于 10 的用户:

    假设我们有一个 orders 表,包含 user_idorder_id 等字段。如果我们想查询订单数量大于 10 的用户,可以使用 having() 方法:

    1
    2
    3
    4
    5
    6
    7
    public List<Order> getUsersWithMoreThan10Orders() {
    QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
    queryWrapper.select("user_id", "COUNT(*) as order_count")
    .groupBy("user_id")
    .having("COUNT(*) > 10"); // 使用 HAVING 子句进行过滤
    return orderMapper.selectList(queryWrapper);
    }

    这个查询会生成如下 SQL:

    1
    2
    3
    4
    SELECT user_id, COUNT(*) as order_count 
    FROM orders
    GROUP BY user_id
    HAVING COUNT(*) > 10;

    但是如果直接拼接用户输入就会造成 sql注入:

    1
    2
    3
    4
    5
    public List<person> getPersons(String username) {
    QueryWrapper<person> queryWrapper = new QueryWrapper<>();
    queryWrapper.groupBy("id").having("username = '"+username+"'");
    return userMapper.selectList(queryWrapper);
    }

    类似的写法还有:只不过上面那个是字符型注入,下面这个是数字型注入罢了。

    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);

    时间盲注:

    image-20250120203555087

    image-20250120203606647

    (6) inSql()和 notInSql()使用不当:

    insql() 方法用于在 WHERE 子句中添加一个 IN 子查询条件。IN 子句会检查字段的值是否存在于由子查询返回的结果集中。

    假设我们有一个 orders 表,想查询所有 user_id 在某些 order_id 查询结果中的用户。可以使用 inSql() 方法来构造这个查询:

    1
    2
    3
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.inSql("id", "SELECT user_id FROM orders WHERE order_date > '2023-01-01'");
    List<User> users = userMapper.selectList(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文件进行映射产生的注入单独拿出来说,我认为最终还是会归咎到上面几种注入中,所以不过多赘述了。