在 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 到表名的映射表)或严格的正则白名单生成。
三、安全写法总结
- 优先使用
#{}:所有值参数一律使用#{},禁止用${}接收用户输入。 - 结构部分白名单化:排序列、表名、列名等无法参数化的部分,必须经过枚举或正则白名单校验。
- 模糊查询用 CONCAT:
LIKE CONCAT('%', #{kw}, '%')。 - IN 查询用 foreach:避免字符串拼接。
- 最小权限原则:数据库账号仅授予必要权限,避免 DROP、FILE 等高危权限。
- 输入校验与长度限制:对参数做类型、长度、格式校验,作为纵深防御。
- 统一异常处理:避免将 SQL 异常信息直接返回给前端,防止信息泄露辅助注入。
- 代码审计与静态扫描:使用 SonarQube、Fortify 等工具扫描
${}使用点,纳入 CI 流程。
四、与 XSS、CSRF 的关联思考
Web 安全是一个整体。SQL 注入、XSS、CSRF 虽攻击面不同,但都源于“信任了不可信输入”。SQL 注入危害数据库,XSS 危害前端用户,CSRF 冒用用户身份发起请求。在 MyBatis 层面做好参数化,能消除注入;在输出层做好 HTML 转义,能缓解 XSS;在请求层加入 CSRF Token 与 SameSite Cookie,能防御 CSRF。三者结合,才构成完整的 Web 安全加固体系。
五、结语
MyBatis 本身并不“不安全”,风险来自开发者对 ${} 的滥用。记住一条铁律:值用 #{},结构用白名单。在此基础上配合最小权限、输入校验和持续审计,才能让持久层既灵活又稳固。安全不是一次性的配置,而是贯穿编码、测试、上线全流程的习惯。
未经允许不得转载:任鹏个人博客 » MyBatis 中的 SQL 注入风险与安全写法总结


朋友圈点赞图在线生成源码