防止SQL注入,MyBatis的动态SQL和bind元素是关键防线,但若使用不当,它们本身也可能成为新的注入点。直接说问题:开发者常误以为MyBatis的#{}参数占位符能完全免疫SQL注入,却忽略了在动态SQL的"<if>"、"<choose>"等标签内,如果错误地使用了${}进行字符串拼接,或者在bind元素中直接拼接未经验证的用户输入,依然会导致SQL注入。解决方法是严格遵循“永远使用#{},避免使用${}”的铁律,并在必须使用bind时,确保其value值不直接包含用户输入,而是经过处理的安全值。
理解MyBatis的#{}与${}的本质区别
核心安全机制在于参数占位符#{}与字符串替换${}的原理差异。#{}在MyBatis中会被预处理为JDBC的PreparedStatement的参数占位符(即问号?),传入的参数会进行类型处理和转义,从根本上隔离了SQL指令与数据,从而防止注入。而${}则是简单的字符串替换,MyBatis会将参数值直接拼接到SQL语句中,如果该值包含恶意SQL片段,就会被数据库执行。
<!-- 安全示例:使用#{} -->
<select id="selectUser" parameterType="String" resultType="User">
SELECT * FROM user WHERE username = #{name}
</select>
<!-- 危险示例:使用${}进行拼接 -->
<select id="selectOrder" parameterType="String" resultType="Order">
SELECT * FROM order ORDER BY ${orderByField}
</select>在上面危险示例中,如果"orderByField"参数被用户控制并传入""id; DROP TABLE order --"",将导致灾难性后果。因此,${}仅能用于极少数可信场景,如动态指定排序字段名或表名,且这些值必须由后端代码严格枚举控制,绝不能直接来自前端用户输入。
动态SQL标签中的常见陷阱与防护
在"<if>"、"<choose>"、"<when>"、"<otherwise>"、"<where>"、"<set>"、"<foreach>"等动态SQL标签中,危险往往隐藏在条件判断里。一个典型错误是在OGNL表达式中使用${}进行拼接。
<!-- 错误:在<if>条件中使用${}拼接 -->
<select id="dynamicSearch" parameterType="Map" resultType="User">
SELECT * FROM user WHERE 1=1
<if test="name != null">
AND username = '${name}'
</if>
</select>此处的"'${name}'"是双重错误:既使用了${},又手动添加了引号。正确做法是使用#{},且无需引号。
<!-- 正确:在<if>中使用#{} -->
<select id="dynamicSearch" parameterType="Map" resultType="User">
SELECT * FROM user WHERE 1=1
<if test="name != null">
AND username = #{name}
</if>
</select>对于"<foreach>"标签,用于IN查询时是安全的,因为它迭代集合并为每一项生成一个#{}占位符。
<!-- 安全:<foreach>与#{}结合 -->
<select id="selectUsersInIds" parameterType="list" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>bind元素的正确使用与风险规避
bind元素的本意是创建一个变量并将其绑定到上下文,常用于模糊查询或简化复杂的OGNL表达式。其风险在于,如果在value属性中直接拼接用户输入,就绕过了#{}的保护。
<!-- 危险:bind值直接拼接未过滤的用户输入 -->
<select id="searchUser" parameterType="String" resultType="User">
<bind name="pattern" value="'%' + name + '%'"/>
SELECT * FROM user WHERE username LIKE #{pattern}
</select>假设"name"参数传入""admin' -- "",那么"pattern"的值会变成""%admin' -- %""。虽然最后的LIKE子句使用了#{},但bind过程中已经完成了字符串拼接,恶意单引号已被引入。当SQL执行时,"--"后的内容可能被注释掉,改变查询逻辑。正确的做法是,确保拼接操作在Java代码层面完成,将处理好的安全值作为参数传入,或者在bind中使用OGNL的字符串连接函数,但参数本身需经过预校验。
<!-- 较安全:在Java层处理模糊查询参数 -->
// Java Service层
public List<User> searchUser(String name) {
String safePattern = "%" + name + "%"; // 此处可加入额外的过滤或转义逻辑
return userMapper.searchUser(safePattern);
}
// MyBatis Mapper XML
<select id="searchUser" parameterType="String" resultType="User">
SELECT * FROM user WHERE username LIKE #{pattern}
</select>如果坚持在XML中使用bind,应确保参与拼接的原始参数已经过严格的验证和过滤(如白名单、长度限制、特殊字符转义)。
构建纵深防御体系:超越MyBatis的防护
仅依赖MyBatis的特性是不够的,需要在应用层建立多层防御。首先,实施输入验证与过滤,对用户输入的数据类型、长度、格式(如仅允许字母数字)进行白名单校验。其次,在数据访问层之上,使用参数化查询的ORM框架(如与MyBatis结合)作为基础。第三,实施最小权限原则,数据库连接账户应只拥有必要的最低权限,避免使用root或sa等高权限账号。第四,定期进行代码审计和安全扫描,重点关注Mapper XML文件中${}的使用和bind元素的value来源。最后,在Web应用层部署WAF(Web应用防火墙),以拦截常见的注入攻击模式。
总结:安全编码的最佳实践清单
1. 首选#{},禁用${}:将使用${}视为例外,并需要严格的代码审查和理由说明。
2. 动态SQL中坚持#{}:在"<if>"、"<choose>"等标签的SQL片段内,无条件使用#{}。
3. 审慎使用bind元素:避免在bind的value属性中直接拼接用户输入。优先在Java业务逻辑层完成字符串处理。
4. 强制输入验证:所有进入数据库查询的参数,无论是否使用#{},都应进行业务逻辑层面的有效性校验。
5. 持续依赖项更新:保持MyBatis以及数据库驱动jar包为最新稳定版本,以获取已知安全漏洞的修复。
6. 进行安全测试:在测试阶段引入SQL注入渗透测试,使用自动化工具和手动测试验证接口安全性。
通过将MyBatis的安全特性与严谨的编码规范、多层次的防御策略相结合,才能有效构筑防止SQL注入的坚固堡垒,确保应用数据安全无虞。
