写点什么

Mybatis 常用注解中的 SQL 注入

作者:编程江湖
  • 2022 年 2 月 09 日
  • 本文字数:2984 字

    阅读完需:约 10 分钟

Batis3 提供了新的基于注解的配置。主要在 MapperAnnotationBuilder 中,定义了相关的注解:

public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {    ...    sqlAnnotationTypes.add(Select.class);    sqlAnnotationTypes.add(Insert.class);    sqlAnnotationTypes.add(Update.class);    sqlAnnotationTypes.add(Delete.class);    ......    sqlProviderAnnotationTypes.add(SelectProvider.class);    sqlProviderAnnotationTypes.add(InsertProvider.class);    sqlProviderAnnotationTypes.add(UpdateProvider.class);    sqlProviderAnnotationTypes.add(DeleteProvider.class);}
复制代码

增删改查占据了绝大部分的业务操作,通过注解不在需要配置繁杂的 xml 文件,越来越多的 sql 交互均通过注解来实现。从 MapperAnnotationBuilder 可以看到 Mybatis 提供了以下相关的注解:

  • @Select

  • @Insert

  • @Update

  • @Delete

  • @SelectProvider

  • @InsertProvider

  • @UpdateProvider

  • @DeleteProvider

例如如下例子,使用 @Select 注解直接编写 SQL 完成数据查询:

@Mapperpublic interface UserMapper {    @Select("select * from t_user")    List<User> list();}
复制代码

使用类似 @SelectProvider 高级注解可以指定某个工具类的方法来动态编写 SQL,以应对复杂的业务需求。

以 @SelectProvider 为例,查看具体的实现,主要包含两个注解属性,其中 type 表示工具类,method 表示工具类的某个方法,用于返回具体的 SQL:

@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface InsertProvider {    // 用于指定获取 sql 语句的指定类    Class<?> type();    // 指定类中要执行获取 sql 语句的方法    String method();}
复制代码

使用方法如下,在 ProjectSql 类的 getContentByProjectIds 方法定义相关的 sql 即可,sql 的定义可以通过 org.apache.ibatis.jdbc.SQL 来快速实现:

@SelectProvider(type = ProjectSql.class, method = "getContentByProjectIds")List<Integer> getContentByProjectIds(List<Integer> projectIds);
复制代码


常见注入场景


2.1 普通注解

实际上跟 xml 配置中对应的标签语法是一样的(例如 @Select 对应<select>标签),所以注入场景也是类似的。

在 Mybatis 中,#的作用主要是替换预编译语句(PrepareStatement)中的占位符?,$是直接的 SQL 拼接。以 like 模糊查询 为例子:

例如如下例子:

跟 xml 配置一样,like 模糊查询直接使用 #预编译的方式进行注解的话会触发异常,所以很多时候直接使用 $进行注解:

@Select("SELECT id, name, age, email FROM user where name like '${name}'")List<User> queryUserByName(@Param("name") String name);
复制代码

那么此时 name 前端用户可控的话,将导致 SQL 注入风险。


查看 sql 日志,成功执行 1/0 触发 sql error,说明注入成功:



处理这类 SQL 问题也很简单,使用 sql 的内置函数进行拼接,拼接后再采用 #预编译的方式进行查询。例如上面案例是 h2 数据库的,使用'||'拼接再进行预编译处理即可:

@Select("SELECT id, name, age, email FROM user where name like '%'||#{name}||'%'")List<User> queryUserByName(@Param("name") String name);
复制代码

此时已使用预编译进行 SQL 查询:



此外,类似 Order by、动态表名,无法采用预编译的方式情况,可以在在代码层使用间接引用的方式进行处理。

对于范围查询 in,熟悉 mybatis 注入的话,是需要使用 MyBatis 自带的循环指令 foreach 来解决 SQL 语句动态拼接的,当使用注解时,就需要使用< script>标签来引入 foreach 了。

2.2 动态 sql

2.2.1 使用< script>

要在带普通注解的映射器接口类中使用动态 SQL,可以使用 script 元素。跟 xml 类似,主要是如下的元素:

  • if

  • choose (when, otherwise)

  • trim (where, set)

  • foreach

相关的注入场景跟 2.1 也是类似的。也是离不开 $。此外,在进行同条件多值查询(例如范围查询 in)的时候,可以使用 MyBatis 自带的循环指令 foreach 来解决 SQL 语句动态拼接的问题。

2.2.2 使用 Provider 注解

可以通过使用 Provider 注解指定某个工具类的方法来动态编写 SQL。以 @SelectProvider 为例:

首先在 mapper 中使用 @SelectProvider 定义相关的方法,其中 type 表示工具类,method 表示工具类的某个方法,用于返回具体的 SQL。例如下面的例子:

通过传递 userIds 以及 name,查询相关的用户信息,在 UserInfoSql 类的 getUserInfoByids 方法定义了具体的 SQL 内容:

/**   * @param userIds 必填   * @param name 可选   * @return   */  @SelectProvider(type = UserInfoSql.class, method = "getUserInfoByids")  List<User> getUserInfoByids(List<Long> userIds, String name);     class UserInfoSql {    public String getUserInfoByids(List<Long> userIds, String name) {      SQL sql = new SQL();      sql.SELECT("id, name, age, email");      sql.FROM("user");      sql.WHERE("id in(" + Joiner.on(',').join(userIds) + ")");      if(StringUtil.isNotBlank(name)){        sql.WHERE("name like '%" + name + "%'");      }      sql.ORDER_BY("id desc");      return sql.toString();    }  }
复制代码

在 Controller 调用具体方法就可以进行 sql 查询了:

    @RequestMapping(value = "/getUserInfoByids")    public List<User> getUserInfoByids(String name,@RequestParam List<Long> userIds){            List<User> userList = userMapper.getUserInfoByids(userIds,name);            return userList;    }
复制代码

正常请求返回对应的用户信息:



前面是通过 MyBatis 3 提供的工具类 org.apache.ibatis.jdbc.SQL 来生成 SQL 的。该类提供了类似 select、where、ORDER_BY 等方法来完成 SQL 生成的操作。这里有个误区,很多开发认为这里工具类会进行相关的预编译处理。

实际上 Provider 其实只需要返回一个 SQL 字符串,工具类只不过用了一些关键字做格式化而已,甚至可以直接使用 StringBuffer 拼接 SQL 语句。同样是上面的例子,List userIds 是 long 类型,但是 name 是 String 类型,可以尝试注入:



查看相关日志,成功执行 1/0 逻辑触发 SQL error,也印证了 Provider 实际上只是 SQL 拼接,没有做相关的安全处理 :



相比 @Select@,SelectProvider 只是在定义注解的方式上有所不同, 前者是直接定义 sql, 一个是在外部定义好 sql 直接引用, 没本质上的区别,所以解决方法是在对应的 sql 场景,使用 #进行预编译进行处理,例如这里的 like 模糊查询和 in 范围查询:

@SelectProvider(type = UserInfoSql.class, method = "getUserInfoByids")  List<User> getUserInfoByids(@Param("userIds")List<Long> userIds,@Param("name")String name);     class UserInfoSql {    public String getUserInfoByids(@Param("userIds")List<Long> userIds, @Param("name")String name) {            StringBuilder sql = new StringBuilder(128);            sql.append("< script>SELECT id, name, age, email FROM user WHERE (id in");            sql.append("<foreach item='item' collection='userIds' open='(' separator=',' close=')'>#{item}</foreach>");      if(StringUtil.isNotBlank(name)){              sql.append("and name like '%'||#{name}||'%')");      }      sql.append("ORDER BY id desc</script>");      return sql.toString();    }  }
复制代码

查看 sql 日志,此时使用预编译进行 sql 处理,避免了 SQL 注入风险:



关键词:java培训

用户头像

编程江湖

关注

IT技术分享 2021.11.23 加入

还未添加个人简介

评论

发布
暂无评论
Mybatis常用注解中的SQL注入