在 Java 开发中,SQL 注入是一种常见且危害巨大的安全漏洞。本文将针对 Java 常用的几种框架,深入分析 SQL 注入的产生原因、常见漏洞场景及安全解决方案,为开发者和安全审计人员提供参考。
一、SQL 注入存在条件
Java 作为强类型语言,SQL 注入风险的存在具有特定条件:仅当String 类型变量参与 SQL 语句构建时,才可能存在注入风险。若参数被强制限定为
integer
等非字符串类型,当传入非对应类型的值时,会触发类型错误,从而不会产生 SQL 注入。二、JDBC 框架 SQL 注入分析
(一)危险用法:Statement
- 原理:通过字符串拼接的方式构造 SQL 语句,没有预编译机制。当用户可控的输入未经过滤直接拼接到 SQL 语句中时,攻击者可以篡改 SQL 逻辑,实现注入攻击。
- 示例代码:
// Web接口场景
@RequestMapping("/jdbc/vuln")
public String jdbc_sqli_vul(@RequestParam("username") String username) {
String sql = "select * from users where username = '" + username + "'";
Statement statement = con.createStatement();
ResultSet rs = statement.executeQuery(sql);
return "查询结果";
}
- 漏洞验证:当输入
joychou' or '1'='1
时,生成的 SQL 语句为select * from users where username = 'joychou' or '1'='1'
,该语句会返回全部用户数据,造成信息泄露。
(二)安全用法:PreparedStatement
- 原理:采用预编译机制,使用
?
作为占位符,通过setXxx()
方法绑定参数。这种方式会将用户输入作为数据处理,而不是 SQL 语句的一部分,因此无法改变 SQL 结构,能有效防止注入。 - 示例代码:
String sql = "select * from users where username = ?";
PreparedStatement st = con.prepareStatement(sql);
st.setString(1, username);
ResultSet rs = st.executeQuery();
- 优势:不仅能防止 SQL 注入,还能减少 SQL 语句的重复编译,提升系统性能。
(三)高频漏洞场景
漏洞场景 | 危险代码示例 | 安全解决方案 |
---|---|---|
未用占位符 | sql = "select * from users where id=?" + " and username like '%" + username1 + "%'"; |
全量使用占位符,如sql = "select * from users where id=? and username like ?"; ,然后绑定参数 |
in 语句拼接 |
String sql = "delete from users where id in(" + delIds + ")"; |
遍历生成占位符或使用foreach 循环绑定参数 |
like 语句拼接 |
String sql = "select * from users where password like '%" + con + "%'"; |
使用数据库函数拼接,如 MySQL 的like concat('%', #{param}, '%') |
order by 无预编译 |
String sql = "select * from news where title =?" + "order by '" + time + "' asc"; |
用字段列数预编译或严格过滤字段名白名单 |
未过滤通配符 | 用户输入含% 导致like '%a%' 全匹配 |
手动过滤% 和_ 通配符 |
三、MyBatis 框架 SQL 注入分析
(一)核心风险点:#{} 与 ${}
特性 | #{ }(安全) | ${ }(危险) |
---|---|---|
处理方式 | 转换为预编译? ,进行参数绑定 |
直接进行字符串拼接,没有预编译过程 |
防注入能力 | 支持 | 不支持 |
适用场景 | 普通参数(如where id=#{id} ) |
必须进行拼接的场景(如order by ${column} ,此时需要额外过滤) |
(二)高频漏洞场景
1. 模糊查询
- 危险用法:由于
like '%#{param}%'
存在语法错误,很多开发者会改用${ }
进行拼接,从而引入风险:
<select id="findByUserNameVuln" resultType="User">
select * from users where username like '%${_parameter}%'
</select>
- 安全用法:使用数据库函数实现预编译,避免直接拼接:
<select id="findByUserNameSec" resultType="User">
select * from users where username like concat('%', #{_parameter}, '%')
</select>
2. in
多值查询
- 危险用法:直接用
${ }
拼接in
语句,会导致注入风险:
<select id="findByUserNameVuln04" resultType="User">
select * from users where id in (${id})
</select>
- 安全用法:使用
foreach
循环生成占位符,实现参数绑定:
<select id="selectBookByIds" parameterType="list" resultType="Book">
SELECT * FROM Book WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
3. order by
排序
- 危险用法:因为
#{ }
会给字段加引号,导致排序失效,所以错误地使用${ }
进行拼接:
<select id="getUsersBySort" resultType="User">
SELECT * FROM users ORDER BY ${sortColumn}
</select>
- 安全用法:可以采用字段列数预编译的方式,或者严格过滤字段名,只允许白名单中的字段参与排序。
(三)实战审计案例
以某医院管理系统(Spring Boot + MyBatis)为例:
- 审计入口:在 Mapper 文件中搜索
${
关键字,定位可能存在风险的代码位置。 - 漏洞代码:发现存在
SELECT distinct ${column} FROM users
这样的代码,其中column
由路由参数传入,攻击者可以控制该参数。 - 验证 Payload:通过访问
http://xxx/option/users/(select%20database())
,成功获取到数据库名,证实了 SQL 注入漏洞的存在。
四、MyBatis-Plus 框架 SQL 注入分析
(一)安全机制
QueryWrapper
/UpdateWrapper
的基础方法(如eq
、like
、in
等)会自动进行预编译处理,有效防止 SQL 注入:@RequestMapping("/like")
public List<Tutorial> mybatislike(String author) {
QueryWrapper<Tutorial> wrapper = new QueryWrapper<>();
wrapper.like("author", author);
return tutorialMapper.selectList(wrapper);
}
(二)高频漏洞场景
危险方法 | 漏洞代码示例 | 安全解决方案 |
---|---|---|
apply() 直接拼接 |
wrapper.apply("title=" + title); |
使用{index} 占位符绑定参数 |
last() 拼接尾部 |
wrapper.last("order by " + column); |
过滤排序字段白名单 |
exists() /notExists() |
wrapper.exists("select title from t where title = " + title); |
使用占位符绑定参数 |
having() 拼接条件 |
wrapper.having("id > " + id); |
使用{index} 占位符 |
orderBy() /groupBy() |
wrapper.orderByAsc(column); |
过滤字段名白名单 |
分页插件addOrder() |
personPage.addOrder(OrderItem.asc(order)); |
过滤order 参数 |
(三)自定义 SQL 风险
在使用
#{ew.customSqlSegment}
时,如果Wrapper
中包含未进行预编译的方法(如last
、orderBy
等),可能会引发 SQL 注入漏洞。五、Hibernate/JPA 框架 SQL 注入分析
(一)危险用法
- HQL 注入示例:
String userInput = "2 or 1=1";
String hql = "from Book where id = " + userInput;
Query<Book> query = session.createQuery(hql, Book.class);
Book book = query.uniqueResult();
- 原生 SQL 注入示例:
String sql = "select * from user where username = '" + username + "'";
Query<People> query = session.createNativeQuery(sql);
(二)安全用法
安全方式 | 示例代码 | 适用场景 |
---|---|---|
命名参数绑定 | String hql = "from users where name = :name"; query.setParameter("name", parameter); |
复杂查询 |
位置参数绑定 | String hql = "from users where name = ?1"; query.setParameter(1, parameter); |
简单查询 |
命名参数列表 | query.setParameter("names", Arrays.asList("g1ts", "g2ts")); |
in 多值查询 |
Criteria API | CriteriaQuery<Book> cq = cb.createQuery(Book.class); cq.where(cb.equal(root.get("id"), bookId)); |
动态多条件查询 |
session.get() |
Book book = session.get(Book.class, bookId); |
主键查询 |
(三)HQL 注入限制
HQL 注入相比原生 SQL 注入,存在一些限制:
- 不支持
UNION SELECT
语句; - 无法访问系统表;
- 需要知晓实体类的字段名才能进行有效的注入;
- 不支持
--
注释,部分情况下支持/* comment */
注释。
通过了解以上各框架中 SQL 注入的风险点和安全用法,开发者可以在实际开发中采取相应的防范措施,有效降低 SQL 注入漏洞的发生概率,提高系统的安全性。