作为Java生态中最主流的ORM框架之一,Mybatis凭借其灵活的SQL编写方式和优秀的性能表现,已经成为企业级应用开发的标准配置。但正是这种"半自动化"的特性,使得开发者在享受SQL编写自由度的同时,也面临着严峻的SQL注入风险。我见过太多团队因为忽视这个问题,导致数据库被拖库、用户信息泄露等安全事故。
SQL注入本质上是一种通过构造特殊输入参数,改变原始SQL语义的攻击方式。在传统的JDBC编程中,字符串拼接是最典型的注入漏洞来源。而Mybatis虽然提供了#{}和${}两种参数注入方式,但很多初学者往往分不清它们的区别,错误使用${}导致注入漏洞。更棘手的是,即使正确使用了#{},在某些特殊场景下(如动态表名、排序字段等)仍然可能被迫使用${},这就需要我们掌握更全面的防护策略。
先看一个典型的安全案例:
java复制@Select("SELECT * FROM users WHERE username = #{name} AND password = #{pass}")
User login(@Param("name") String name, @Param("pass") String pass);
这里的#{}会被Mybatis预处理为参数化查询,等价于JDBC的PreparedStatement,输入参数会被当作纯数据处理,不会改变SQL结构。
而危险的使用方式:
java复制@Select("SELECT * FROM users WHERE username = '${name}'")
User findUser(@Param("name") String name);
当传入name = "admin' OR '1'='1"时,最终执行的SQL变为:
sql复制SELECT * FROM users WHERE username = 'admin' OR '1'='1'
这将返回所有用户数据,造成严重的信息泄露。
Mybatis的动态SQL功能虽然强大,但也隐藏着注入隐患。比如常见的<if>条件判断:
xml复制<select id="searchUsers" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null">
AND username LIKE '%${name}%'
</if>
</select>
这里的LIKE查询如果使用${}拼接,攻击者可以构造特殊输入破坏查询逻辑。正确的做法应该是:
xml复制<if test="name != null">
AND username LIKE CONCAT('%', #{name}, '%')
</if>
有些场景确实无法避免使用${},比如动态表名:
java复制@Select("SELECT * FROM ${tableName} WHERE id = #{id}")
User findById(@Param("tableName") String tableName, @Param("id") Long id);
针对这种情况,我总结出三层防护策略:
java复制private static final Set<String> ALLOWED_TABLES = Set.of("users", "products");
public User findByIdSafe(String tableName, Long id) {
if (!ALLOWED_TABLES.contains(tableName)) {
throw new IllegalArgumentException("Invalid table name");
}
return mapper.findById(tableName, id);
}
java复制tableName = tableName.replaceAll("[^a-zA-Z0-9_]", "");
我们可以通过自定义插件,在运行时拦截和检查所有SQL:
java复制@Intercepts({
@Signature(type= StatementHandler.class,
method="prepare",
args={Connection.class, Integer.class})
})
public class SqlInjectionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
// 检测SQL中的${}使用
if (boundSql.getSql().contains("${")) {
log.warn("Potential SQL injection risk: " + boundSql.getSql());
// 可以加入邮件报警等机制
}
return invocation.proceed();
}
}
在mybatis-config.xml中配置:
xml复制<plugins>
<plugin interceptor="com.example.SqlInjectionInterceptor"/>
</plugins>
在实际企业环境中,我建议建立多层次的防御:
输入层:
DAO层:
数据库层:
我们团队制定的安全编码规范包括:
静态代码扫描:
单元测试要求:
java复制@Test
void testSqlInjection() {
// 测试注入攻击
assertThrows(Exception.class, () -> {
userMapper.findByUsername("admin' --");
});
// 测试特殊字符处理
User user = userMapper.findByUsername("normal_user");
assertNotNull(user);
}
安全评审机制:
即使是使用#{},在某些复杂场景下也需要特别注意:
java复制@Select("SELECT * FROM users WHERE id IN (${ids})")
List<User> findByIds(@Param("ids") String ids);
这种IN查询的正确处理方式应该是:
方案1:使用MyBatis的动态SQL
xml复制<select id="findByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
方案2:使用数据库特定函数(MySQL示例)
java复制@Select("SELECT * FROM users WHERE FIND_IN_SET(id, #{ids}) > 0")
List<User> findByIdsSafe(@Param("ids") String ids);
对于复杂业务逻辑,可以考虑使用存储过程:
java复制@Options(statementType = StatementType.CALLABLE)
@Select("{call sp_get_user_by_credentials(#{name}, #{pass})}")
User loginByProcedure(@Param("name") String name, @Param("pass") String pass);
存储过程的优势在于:
让我们通过一个完整案例来演示安全审计过程:
漏洞代码:
java复制@Select("SELECT * FROM products WHERE name LIKE '%${keyword}%' " +
"ORDER BY ${sortField} ${sortOrder}")
List<Product> searchProducts(@Param("keyword") String keyword,
@Param("sortField") String sortField,
@Param("sortOrder") String sortOrder);
攻击方式:
code复制keyword = test'; DROP TABLE products; --
sortField = (SELECT CASE WHEN (SELECT current_user)='dbadmin' THEN name ELSE id END)
sortOrder = DESC
修复方案:
java复制@Select("SELECT * FROM products WHERE name LIKE CONCAT('%', #{keyword}, '%')")
List<Product> searchProductsSafe(@Param("keyword") String keyword);
java复制private String validateSortField(String field) {
return Arrays.asList("name", "price", "create_time").contains(field) ? field : "id";
}
private String validateSortOrder(String order) {
return "DESC".equalsIgnoreCase(order) ? "DESC" : "ASC";
}
java复制@SelectProvider(type = ProductSqlBuilder.class, method = "buildSearchSql")
List<Product> searchProductsSafe(@Param("keyword") String keyword,
@Param("sortField") String sortField,
@Param("sortOrder") String sortOrder);
// 在SqlBuilder中实现安全控制
class ProductSqlBuilder {
public String buildSearchSql(Map<String, Object> params) {
String keyword = (String) params.get("keyword");
String sortField = validateSortField((String) params.get("sortField"));
String sortOrder = validateSortOrder((String) params.get("sortOrder"));
return new SQL() {{
SELECT("*");
FROM("products");
if (StringUtils.isNotBlank(keyword)) {
WHERE("name LIKE CONCAT('%', #{keyword}, '%')");
}
ORDER_BY(sortField + " " + sortOrder);
}}.toString();
}
}
在生产环境中,我们需要建立有效的监控机制:
日志分析规则:
--、;、'、/*等特殊字符的请求WAF规则配置:
nginx复制location /api {
# 阻止常见SQL注入特征
if ($args ~* "union.*select|select.*from|insert.*into|delete.*from") {
return 403;
}
proxy_pass http://app_server;
}
一旦发现注入攻击,应立即:
最后分享一个我团队使用的自查表,每个Mybatis方法都需要经过这些检查:
记住,安全不是一次性的工作,而是需要持续关注的系统工程。每次代码变更、每个新功能上线,都应该把SQL注入防护作为首要考虑因素。