导读:本期聚焦于剑客创作的《MyBatis中如何防止SQL注入风险?为什么要把${}替换成#{}占位符》,敬请观看详情。当MyBatis的mapper文件里出现${username}时,外部输入会被直接拼接到SQL语句里,攻击者传入admin' or '1'='1就能轻松绕过登录校验。${}和#{}虽然都写在XML里,但执行机制完全不同。#{}会让MyBatis把SQL交给JDBC的PreparedStatement处理,参数位置变成?占位符,由驱动负责对值做类型转换和转义,输入内容不会破坏SQL结构。${}则是在SQL字符串层面做替换,等于放弃预编译保护。很多项目在被安全扫描标记前,根本没意识到order by ${column}这类写法会变成注入入口。要修复风险,第一步是把所有值绑定位置改成#{},同时理解#{}不能用于表名、字段名、排序方向等结构标识。对于确实需要动态拼接的结构,必须用白名单校验后再拼接。本文会拆解两种占位符的运行流程,结合登录、排序、like查询等典型场景,给出可落地的改造方法和安全兜底。

MyBatis的SQL映射文件里,${}和#{}是两种常用的参数占位符。很多人一开始会以为它们只是写法不同,实际上一个走字符串替换,一个走预编译参数绑定。这个差异直接决定了接口是否存在SQL注入风险。以一个最常见的登录查询为例,先看危险写法。

MyBatis中如何防止SQL注入风险?为什么要把${}替换成#{}占位符

上面这段mapper里,username和password都用${}包着。MyBatis在处理这种写法时,会直接把变量值填进SQL字符串,再交给数据库执行。如果攻击者在用户名框输入admin' or '1'='1,最终SQL会变成SELECT * FROM sys_user WHERE username = 'admin' or '1'='1' AND password = ''。数据库会先执行or '1'='1这个恒真条件,后面的密码校验完全失效。把${}改成#{}以后,同样的输入只会被当作一个普通字符串参数绑定到?占位符上,单引号和or不再具备SQL语法含义。

<select id="findUserByLogin" parameterType="map" resultType="User">
    SELECT * FROM sys_user
    WHERE username = '${username}'
      AND password = '${password}'
</select>
<select id="findUserByLogin" parameterType="map" resultType="User">
    SELECT * FROM sys_user
    WHERE username = #{username}
      AND password = #{password}
</select>

一、两种占位符在MyBatis内部的处理差异

MyBatis解析<select>节点时,如果发现SQL里包含${},会先把变量值通过字符串拼接的方式塞进SQL里,然后再把这个已经固定下来的SQL交给JDBC执行。这意味着参数值会参与SQL语法解析。比如传入admin' or '1'='1,拼接后单引号出现的位置直接改变了原始SQL的条件结构。

而#{}的处理方式完全不同。MyBatis会先把SQL里的#{}替换成JDBC的?占位符,随后通过PreparedStatement的setString、setInt等方法把参数值绑定到对应位置。参数值只作为数据处理,不参与SQL语法解析。JDBC驱动在绑定字符串时会自动处理单引号等特殊字符,因此即使值里包含单引号,也不会截断原本的SQL字符串。

从日志里能更直观地看到这个过程。使用#{}时,MyBatis的输出大致是这样的:

==> Preparing: SELECT * FROM sys_user WHERE username = ? AND password = ?
==> Parameters: admin' or '1'='1(String), 123456(String)

SQL结构在准备阶段就已经固定,参数是后面单独传进去的。如果换成${},日志里打印的Preparing语句会直接包含攻击载荷,数据库根本不知道哪些是原始SQL、哪些是用户输入。

二、#{}为什么能阻断SQL注入

SQL注入的本质是用户输入被当成SQL代码执行。要形成注入,攻击者必须破坏原有SQL语句的语法边界,比如提前闭合字符串引号,或者追加新的条件、注释符。${}直接把值拼进SQL里,等于给攻击者提供了修改语法的机会。比如输入admin' --,单引号会提前闭合,随后--把后面的密码条件注释掉,整个查询就只剩下用户名条件。

#{}通过预编译参数绑定绕开了这个问题。数据库先解析一次带?的SQL模板,确定好查询哪些表、哪些字段、哪些条件,之后才接收实际参数。参数即使包含单引号、分号、注释符,也只会被当成一个字符串值。以MySQL驱动为例,字符串绑定时会自动对单引号做转义,例如传入a'b,最终存储和比较的值就是a'b,不会因为这个单引号改变SQL结构。这就是预编译带来的天然防护。

还有一点值得注意,#{}会保留参数类型信息。比如传整型参数时,MyBatis会调用setInt,传字符串时调用setString。类型明确后,数据库不会对参数内容再做隐式类型转换,也减少了因类型转换导致的绕过风险。而${}拼接后,所有内容都是字符串,可能出现意料之外的隐式转换。

like查询也是一个典型场景。如果不注意,可能会写出LIKE '%${keyword}%',用户输入%' or '1'='1就能再次注入。改成LIKE CONCAT('%', #{keyword}, '%')后,百分号只作为普通字符拼在SQL里,用户输入完全走参数绑定,风险就消除了。

三、不能无脑替换的场景与白名单方案

#{}虽然安全,但并不是所有位置都能用。像表名、字段名、排序方向这些SQL结构部分,使用#{}会出问题。例如ORDER BY #{column}会被数据库理解成ORDER BY 'username',列名被当成字符串常量,排序效果完全不对。所以动态排序字段有时不得不使用${}。

但这不代表可以放心使用${}。只要这些结构部分来自用户输入,就必须做白名单校验。例如只允许按照id、username、create_time排序,代码可以这样处理:

private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "username", "create_time");

public List<User> selectByOrder(String orderBy) {
    if (orderBy == null || !ALLOWED_COLUMNS.contains(orderBy)) {
        throw new IllegalArgumentException("非法排序字段");
    }
    return mapper.selectByOrder(orderBy);
}

对应的XML仍然可以使用${},但此时传入的值已经经过严格校验,只可能是白名单里的三个列名之一。

<select id="selectByOrder" parameterType="string" resultType="User">
    SELECT * FROM sys_user
    ORDER BY ${orderBy} DESC
</select>

动态表名的情况类似。如果业务确实需要根据租户编码选择不同的物理表,不要直接拼接用户传入的字符串,可以维护一个租户编码到表名的映射关系,根据编码查到合法表名后再拼接。这样即便攻击者提交恶意表名,也无法通过映射找到对应表,SQL注入自然被阻断。

四、从漏洞整改到回归验证的完整流程

整改时第一步是全局搜索mapper文件里的${}。在IDEA、VS Code或Notepad++里搜索${即可命中所有位置。逐个判断它属于值绑定还是结构绑定。值绑定一律改成#{},例如where条件里的用户名、密码、id、状态等。结构绑定则按照前面说的白名单方案处理。

修改完成后,不要直接上线,要用典型注入载荷做回归测试。例如登录接口可以测试admin' --、admin' or '1'='1、admin' or 1=1 --等输入。观察返回结果和SQL日志,确认这些输入没有改变查询条件。排序接口则要测试非法列名,例如传入username; DROP TABLE sys_user; --,看服务端是否抛出IllegalArgumentException,并且数据库表没有被删除。

如果项目使用logback或log4j输出MyBatis日志,可以临时开启stdout日志。比如在C:\dev\mybatis\mybatis-config.xml里设置logImpl=STDOUT_LOGGING,观察控制台输出的Preparing语句。修复后应该看到问号占位符,而不是已经拼好的恶意SQL。这个路径示例使用反斜杠,不同项目环境按实际部署位置调整即可。

最后,对于历史遗留项目,不推荐一次性修改所有${},因为有些结构拼接依赖运行时才能确定,可能牵涉大量回归。建议按模块推进,优先修复用户直接可控的查询条件、登录、检索、导出接口。每修完一个模块,就补充对应的注入用例到自动化测试里。长期看,还应该在代码评审中把mapper文件里的${}作为重点检查项,尤其是出现在where、order by、group by后面的情况。

MyBatisSQL注入预编译修改时间:2026-09-24 22:26:49

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0924/61471.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。