MyBatis安全防护:SQL注入防护与动态表名处理实战

一、SQL注入风险与防护基础

SQL注入是Web应用中最常见的安全漏洞之一,攻击者通过构造特殊输入改变SQL语义,可能导致数据泄露、篡改甚至服务器沦陷。MyBatis作为半自动ORM框架,其SQL编写方式直接影响应用安全性。

1.1 #{}${}的本质区别

// 安全写法
@Select("SELECT * FROM users WHERE id = #{userId}")
User getUserById(@Param("userId") String userId);

// 危险写法(存在注入风险)
@Select("SELECT * FROM users WHERE id = ${userId}") 
User getUserByIdUnsafe(@Param("userId") String userId);

核心差异

  • #{}:参数化查询(PreparedStatement)

    • 输入值会被正确处理为参数,不会改变SQL结构
    • 自动进行类型转换和特殊字符转义
  • ${}:字符串替换(直接拼接)

    • 原样替换为字符串,可能改变SQL语义
    • 无任何防护措施

1.2 安全编码实践建议

  1. 默认使用#{}:95%的场景都应使用参数化方式
  2. ${}的合法场景

    • 动态表名/列名(需额外防护)
    • SQL关键字(ORDER BY等)
  3. 输入校验:即使使用#{}也应做业务层校验

二、动态表名/列名的安全处理

当业务需要动态指定表名或列名时(如多租户系统),必须谨慎处理以避免注入风险。

2.1 白名单校验方案

<!-- 安全示例:通过白名单控制可访问的表 -->
<select id="queryByTable" resultType="map">
  SELECT * FROM 
  <choose>
    <when test="tableName == 'users'">users</when>
    <when test="tableName == 'products'">products</when>
    <otherwise>default_table</otherwise>
  </choose>
  WHERE id = #{id}
</select>

2.2 程序代码校验方案

public List<Map<String, Object>> queryDynamicTable(String tableName, Long id) {
    // 校验表名合法性
    if (!isValidTableName(tableName)) {
        throw new IllegalArgumentException("Invalid table name");
    }
    
    // 使用${}但已确保安全
    return sqlSession.selectList(
        "com.example.mapper.DynamicMapper.queryByTable", 
        Map.of("tableName", tableName, "id", id));
}

private boolean isValidTableName(String name) {
    return name.matches("[a-zA-Z_][a-zA-Z0-9_]*");
}

2.3 高级防护方案(拦截器)

@Intercepts({
    @Signature(type= StatementHandler.class,
              method="prepare",
              args={Connection.class, Integer.class})
})
public class TableCheckInterceptor implements Interceptor {
    private static final Set<String> ALLOWED_TABLES = Set.of("users", "products");
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        String sql = handler.getBoundSql().getSql();
        
        // 检测SQL中的动态表名
        if (sql.contains("${table}")) {
            String tableName = /* 提取参数值 */;
            if (!ALLOWED_TABLES.contains(tableName)) {
                throw new SQLException("Illegal table access");
            }
        }
        return invocation.proceed();
    }
}

三、综合防护策略

3.1 防御体系分层

图1

  1. 输入层

    • 正则表达式校验
    • 长度/格式限制
  2. 持久层

    • 严格使用#{}
    • 必须使用${}时配合白名单
  3. 数据库层

    • 应用账号最小权限
    • 敏感表单独授权

3.2 MyBatis特殊场景处理

LIKE查询安全写法

<select id="searchUsers" resultType="User">
  SELECT * FROM users 
  WHERE name LIKE CONCAT('%', #{keyword}, '%')
</select>

IN查询安全写法

<select id="getByIds" resultType="User">
  SELECT * FROM users 
  WHERE id IN 
  <foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

四、审计与监控

  1. SQL日志审计

    • 开启MyBatis的debug日志
    • 监控非常规SQL模式
  2. 安全扫描

    • 使用SQLMap等工具定期扫描
    • 代码静态分析(如SonarQube)
  3. 应急响应

    • 建立SQL注入的识别流程
    • 准备数据回滚方案

最佳实践总结

  1. 禁用特性清单

    • 避免使用${}拼接WHERE条件
    • 禁止前端直接传递SQL片段
  2. 安全开发流程

    • 代码审查重点检查SQL写法
    • DAO层单元测试包含注入测试用例
  3. 框架升级

    • 及时更新MyBatis版本
    • 关注安全公告(CVE漏洞)

通过合理使用MyBatis的特性并建立多层防御,可以显著降低SQL注入风险,建议将上述防护措施纳入项目开发规范,并通过自动化工具确保执行。

添加新评论