MyBatis 中的 SQL 注入风险与安全写法总结

在 Java 持久层框架中,MyBatis 因其灵活的 SQL 控制能力被广泛使用。然而,这种灵活性也带来了不容忽视的 SQL 注入风险。与 Hibernate 等全自动 ORM 框架不同,MyBatis 允许开发者直接编写 SQL 片段,如果使用不当,攻击者便可通过构造恶意输入改变 SQL 语义,进而窃取、篡改或删除数据库中的数据。本文将从 MyBatis 的 SQL 注入原理出发,系统梳理常见风险场景,并给出可落地的安全写法。

一、MyBatis 中 SQL 注入的本质

SQL 注入的核心在于“用户输入被当作 SQL 代码执行”。在 MyBatis 中,是否发生注入,关键看参数是预编译占位符还是字符串拼接

MyBatis 提供了两种参数引用方式:

  • #{}:生成 JDBC PreparedStatement 的 ? 占位符,参数值经数据库驱动安全转义,不会改变 SQL 结构
  • ${}:直接将参数值拼接到 SQL 文本中,存在注入风险

因此,一个基本结论是:凡是使用 ${} 的地方,都要逐一审查是否可被用户控制。

二、常见的高危场景

1. 模糊查询中的拼接

很多开发者习惯这样写模糊查询:

<select id="searchUser" resultType="User">
  SELECT * FROM user
  WHERE username LIKE '%${keyword}%'
</select>

keyword 传入 ' OR '1'='1 时,SQL 变为:

SELECT * FROM user WHERE username LIKE '%' OR '1'='1%'

条件恒真,全表数据被泄露。正确做法是使用 #{} 配合 CONCAT

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

2. ORDER BY 与动态列名

由于 #{} 会生成占位符,而占位符不能用于表名、列名、排序方向等 SQL 结构部分,开发者往往被迫使用 ${}

<select id="listUser" resultType="User">
  SELECT * FROM user
  ORDER BY ${sortField} ${sortOrder}
</select>

sortField 可被用户控制,攻击者可注入子查询甚至 UNION 语句。安全做法是白名单校验

private static final Set<String> ALLOWED_SORT_FIELDS =
    Set.of("id", "username", "create_time");
private static final Set<String> ALLOWED_ORDERS =
    Set.of("ASC", "DESC");

public String buildOrderBy(String field, String order) {
    if (!ALLOWED_SORT_FIELDS.contains(field)) {
        throw new IllegalArgumentException("非法排序字段");
    }
    if (!ALLOWED_ORDERS.contains(order.toUpperCase())) {
        throw new IllegalArgumentException("非法排序方向");
    }
    return field + " " + order;
}

3. IN 查询的拼接

部分开发者用 ${} 拼接 IN 列表:

WHERE id IN (${ids})

ids 来自前端,攻击者可传入 1) OR (1=1 等构造。应改用 <foreach> 配合 #{}

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

4. 动态表名与多租户场景

多租户系统中常按租户分表,表名动态拼接:

SELECT * FROM ${tableName} WHERE tenant_id = #{tenantId}

此时 tableName 绝不能直接来自请求参数。应通过服务端映射(如租户 ID 到表名的映射表)或严格的正则白名单生成。

三、安全写法总结

  1. 优先使用 #{}:所有值参数一律使用 #{},禁止用 ${} 接收用户输入。
  2. 结构部分白名单化:排序列、表名、列名等无法参数化的部分,必须经过枚举或正则白名单校验。
  3. 模糊查询用 CONCATLIKE CONCAT('%', #{kw}, '%')
  4. IN 查询用 foreach:避免字符串拼接。
  5. 最小权限原则:数据库账号仅授予必要权限,避免 DROP、FILE 等高危权限。
  6. 输入校验与长度限制:对参数做类型、长度、格式校验,作为纵深防御。
  7. 统一异常处理:避免将 SQL 异常信息直接返回给前端,防止信息泄露辅助注入。
  8. 代码审计与静态扫描:使用 SonarQube、Fortify 等工具扫描 ${} 使用点,纳入 CI 流程。

四、与 XSS、CSRF 的关联思考

Web 安全是一个整体。SQL 注入、XSS、CSRF 虽攻击面不同,但都源于“信任了不可信输入”。SQL 注入危害数据库,XSS 危害前端用户,CSRF 冒用用户身份发起请求。在 MyBatis 层面做好参数化,能消除注入;在输出层做好 HTML 转义,能缓解 XSS;在请求层加入 CSRF Token 与 SameSite Cookie,能防御 CSRF。三者结合,才构成完整的 Web 安全加固体系。

五、结语

MyBatis 本身并不“不安全”,风险来自开发者对 ${} 的滥用。记住一条铁律:值用 #{},结构用白名单。在此基础上配合最小权限、输入校验和持续审计,才能让持久层既灵活又稳固。安全不是一次性的配置,而是贯穿编码、测试、上线全流程的习惯。

未经允许不得转载:任鹏个人博客 » MyBatis 中的 SQL 注入风险与安全写法总结

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏