1. 问题描述

SQL 注入是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息

SQL 注入问题只在 SQL 的预编译过程中才能起作用

2. 问题发生场景

在日常开发中sql操作是每个系统必备的, 不好的编码习惯会导致 SQL 注入的存在, 出现 SQL 注入的场景:

  • 拼接 SQL 语句
  • Mybatis 中 ${} 不正当使用

3. 拼接 SQL 语句

在编写 SQL 语句时,通过占位符的形式, 会将sql进行预编译后,这样传入的参数不会参与预编译过程, 也就不会发生 SQL 注入问题, 使用下面代码会存在 SQL 注入风险

String query = "SELECT * FROM t_user WHERE name = '" + userName + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query);

正确的编码方式如下

PreparedStatement statement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");
statement.setString(1, userName);
ResultSet resultSet = statement.executeQuery();

4. Mybatis 中 ${} 不正当使用

在 Mybatis 中有两种情况经常使用 ${}, 分别是 like, order by

4.1 Like 场景

select * from t_user where name like '%${userName}%'

4.1.1 修复方案

  • mysql
select * from t_user where name like concat('%', #{userName}, '%')
select * from t_user where name like '%'||#{userName}||'%'
  • oracle
select * from t_user where name like '%'||#{userName}||'%'
  • SQL Server
select * from t_user where name like '%' + #{userName} + '%'

4.2 Order By 场景

select * from t_user where name like '%${name}%'

4.2.1 修复方案

order by 场景是无法像 like 那样去调整写法规避 SQL 注入, 对于这种只能用 ${} 场景, 需要在代码里面做白名单验证处理

select * from t_user where name = #{name} order by ${orderByName}
  • 内部转换
/**  
 * 修复 order by 场景 SQL Injection  
 */private final static Map<Integer, String> ODER_NAME_MAP = new HashMap<>(3);
static {
    ODER_NAME_MAP.put(1, "name");  
    ODER_NAME_MAP.put(2, "age");  
    ODER_NAME_MAP.put(3, "id");
}

@GetMapping(value = "orderBy")  
public ResponseVO orderBy(@RequestParam("name") String name, @RequestParam("orderNameId") String orderNameId) {  
    // 通过内部转换形式, 从接口传入 1, 2, 3 在内部分别转换表的字段名 name, age, id    String realOrderName = ODER_NAME_MAP.get(orderNameId);  
    // 参数转换失败, 入参非法  
    if (StringUtils.isBlank(realOrderName)) {  
        return ResponseVO.fail(ResponseCodeEnum.FAIL_JOB_CHECK_ERROR,null);  
    }  
    List<User> userList = userService.queryByOrderByName(realOrderName, name);  
    return ResponseVO.success(userList);  
}
  • 白名单校验
/**  
 * Order By 场景白名单检查  
 */  
private final static Set<String> ORDER_NAME_CHECK_SET = new HashSet<>(3);
static {
	ORDER_NAME_CHECK_SET.add("name");  
	ORDER_NAME_CHECK_SET.add("age");  
	ORDER_NAME_CHECK_SET.add("id");
}

@GetMapping(value = "orderByWhiteLIst")  
public ResponseVO orderByWhiteLIst(@RequestParam("name") String name, @RequestParam("orderName") String orderName) {  
    // 如果传入的表名不在白名单范围内,直接返回参数校验不通过  
    if (!ORDER_NAME_CHECK_SET.contains(orderName)) {  
        // 返回参数校验不通过  
        return ResponseVO.fail(ResponseCodeEnum.FAIL_JOB_CHECK_ERROR,null);  
    }  
    List<User> userList = userService.queryByOrderByName(orderName, name);  
    return ResponseVO.success(userList);  
}

## 4.3 Table Name 场景

select * from ${tableName} where name = #{name}

4.3.1 修复方案

  • 内部转换
/**  
 * 修复 table name 场景 SQL Injection  
 */private final static Map<Integer, String> TABLE_NAME_MAP = new HashMap<>(1);
static {  
    TABLE_NAME_MAP.put(1, "t_user");  
}

@GetMapping(value = "tableName")  
public ResponseVO tableName(@RequestParam("tableNameId") String tableNameId, @RequestParam("name") String name) {  
    String realTableName = TABLE_NAME_MAP.get(tableNameId);  
    // 参数转换失败, 入参非法  
    if (StringUtils.isBlank(realTableName)) {  
        return ResponseVO.fail(ResponseCodeEnum.FAIL_JOB_CHECK_ERROR,null);  
    }  
  
    List<User> userList = userService.queryByTableName(realTableName, name);  
    return ResponseVO.success(userList);  
}
  • 白名单校验
/**  
 * Table Name 场景白名单检查  
 */  
private final static Set<String> TABLE_NAME_CHECK_SET = new HashSet<>(1);
static {
    TABLE_NAME_CHECK_SET.add("t_user");  
}

@GetMapping(value = "tableNameWhiteLIst")  
public ResponseVO tableNameWhiteLIst(@RequestParam("tableName") String tableName, @RequestParam("name") String name) {  
    // 如果传入的表名不在白名单范围内,直接返回参数校验不通过  
    if (!TABLE_NAME_CHECK_SET.contains(tableName)) {  
        // 返回参数校验不通过  
        return ResponseVO.fail(ResponseCodeEnum.FAIL_JOB_CHECK_ERROR,null);  
    }  
  
    List<User> userList = userService.queryByTableName(tableName, name);  
    return ResponseVO.success(userList);  
}

更多推荐

Fortify SQL Injection